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..a5f5cdf5aaf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ +### 🔧 Type of changes +- [ ] new bid adapter +- [ ] bid adapter update +- [ ] new feature +- [ ] new analytics adapter +- [ ] new module +- [ ] module update +- [ ] bugfix +- [ ] documentation +- [ ] configuration +- [ ] dependency update +- [ ] tech debt (test coverage, refactorings, etc.) + +### ✨ What's the context? +What's the context for the changes? + +### 🧠 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 hostnames +- [ ] geographic host parameters are NOT required +- [ ] 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/code-path-changes.yml b/.github/workflows/code-path-changes.yml new file mode 100644 index 00000000000..f818d867441 --- /dev/null +++ b/.github/workflows/code-path-changes.yml @@ -0,0 +1,37 @@ +name: Notify Code Path Changes + +on: + pull_request_target: + types: [ opened, synchronize ] + paths: + - '**' + +permissions: + contents: read + +env: + OAUTH2_CLIENT_ID: ${{ secrets.OAUTH2_CLIENT_ID }} + OAUTH2_CLIENT_SECRET: ${{ secrets.OAUTH2_CLIENT_SECRET }} + OAUTH2_REFRESH_TOKEN: ${{ secrets.OAUTH2_REFRESH_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '18' + + - name: Install dependencies + run: npm install axios nodemailer + + - name: Run Notification Script + run: | + node .github/workflows/scripts/send-notification-on-change.js diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..a6852ae7c92 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,60 @@ +name: CodeQL + +on: + pull_request: + branches: [ 'master' ] + schedule: + - cron: '0 3 * * 1' + +permissions: + security-events: write + packages: read + actions: read + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: java-kotlin + build-mode: manual + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: 21 + + - name: Cache Maven packages + uses: actions/cache@v5 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + + - name: Build with Maven + if: matrix.build-mode == 'manual' + run: mvn -B package --file extra/pom.xml + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: '/language:${{ matrix.language }}' diff --git a/.github/workflows/cross-repo-issue.yml b/.github/workflows/cross-repo-issue.yml index c2288da271a..5d2e512d4c6 100644 --- a/.github/workflows/cross-repo-issue.yml +++ b/.github/workflows/cross-repo-issue.yml @@ -2,9 +2,12 @@ name: Cross-repo Issue Creation on: pull_request_target: - types: [closed] + types: [ closed ] branches: - - "master" + - 'master' + +permissions: + contents: read jobs: cross-repo: @@ -12,7 +15,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@v1 + uses: tibdex/github-app-token@v2.1.0 with: app_id: ${{ secrets.XREPO_APP_ID }} private_key: ${{ secrets.XREPO_PEM }} @@ -23,9 +26,10 @@ jobs: github.event.pull_request.merged env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + PR_TITLE: ${{ github.event.pull_request.title }} run: | echo -e "A PR was merged over on PBS-Java\n\n- [https://github.com/prebid/prebid-server-java/pull/${{github.event.number}}](https://github.com/prebid/prebid-server-java/pull/${{github.event.number}})\n- timestamp: ${{ github.event.pull_request.merged_at}}" > msg export msg=$(cat msg) - gh issue create --repo prebid/prebid-server --title "Port PR from PBS-Java: ${{ github.event.pull_request.title }}" \ + gh issue create --repo prebid/prebid-server --title "Port PR from PBS-Java: $PR_TITLE" \ --body "$msg" \ --label auto diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml index 63d1961388d..7f993ade73d 100644 --- a/.github/workflows/docker-image-publish.yml +++ b/.github/workflows/docker-image-publish.yml @@ -1,10 +1,13 @@ name: Publish Docker image for new tag/release on: - workflow_run: - workflows: [Publish release] - types: - - completed + push: + tags: + - '*' + +permissions: + contents: read + packages: write env: REGISTRY: ghcr.io @@ -14,47 +17,57 @@ jobs: build: name: Publish Docker image for new tag/release runs-on: ubuntu-latest - permissions: - contents: read - 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@v5 + - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 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@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 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..7b4df73b80b 100644 --- a/.github/workflows/issue_prioritization.yml +++ b/.github/workflows/issue_prioritization.yml @@ -1,16 +1,18 @@ name: Issue tracking + on: issues: types: - opened - pinned + jobs: track_issue: runs-on: ubuntu-latest 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..d512022413a 100644 --- a/.github/workflows/pr-functional-tests.yml +++ b/.github/workflows/pr-functional-tests.yml @@ -11,23 +11,51 @@ on: types: - created +permissions: + contents: read + actions: read + checks: write + jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ 17 ] + java: [ 21 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' cache: 'maven' java-version: ${{ matrix.java }} - name: Build with Maven - run: mvn -B verify -DskipUnitTests=true -DskipModuleFunctionalTests=true -Dtests.max-container-count=5 -DdockerfileName=Dockerfile --file extra/pom.xml + id: build + run: | + mvn -B verify \ + -DskipUnitTests=true \ + -DskipModuleFunctionalTests=true \ + -Dtests.max-container-count=5 \ + -DdockerfileName=Dockerfile \ + -Dcheckstyle.skip \ + --file extra/pom.xml + + - name: Emitting run result of functional test + if: always() + uses: dorny/test-reporter@v2.5.0 + with: + name: 'Functional tests' + working-directory: 'target/failsafe-reports' + path: 'TEST-*.xml' + reporter: java-junit + use-actions-summary: 'true' + list-suites: 'failed' + list-tests: 'failed' + fail-on-error: true + fail-on-empty: true + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-java-ci.yml b/.github/workflows/pr-java-ci.yml index 79a904c3636..d69d222592f 100644 --- a/.github/workflows/pr-java-ci.yml +++ b/.github/workflows/pr-java-ci.yml @@ -11,22 +11,28 @@ on: types: - created +permissions: + contents: read + actions: read + checks: write + jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ 17 ] + java: [ 21 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' cache: 'maven' + cache-dependency-path: extra/pom.xml java-version: ${{ matrix.java }} - name: Build with Maven diff --git a/.github/workflows/pr-module-functional-tests.yml b/.github/workflows/pr-module-functional-tests.yml index d8f1e925a07..c3b04858677 100644 --- a/.github/workflows/pr-module-functional-tests.yml +++ b/.github/workflows/pr-module-functional-tests.yml @@ -11,26 +11,55 @@ on: types: - created +permissions: + contents: read + actions: read + checks: write + jobs: build: runs-on: ubuntu-latest strategy: matrix: - java: [ 17 ] + java: [ 21 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' cache: 'maven' java-version: ${{ matrix.java }} - name: Build with Maven - run: mvn package -DskipUnitTests=true --file extra/pom.xml + run: mvn package -Dcheckstyle.skip -DskipUnitTests=true --file extra/pom.xml - name: Run module tests - run: mvn -B verify -DskipUnitTests=true -DskipFunctionalTests=true -DskipModuleFunctionalTests=false -Dtests.max-container-count=5 -DdockerfileName=Dockerfile-modules --file extra/pom.xml + id: build + run: | + mvn -B verify \ + -DskipUnitTests=true \ + -DskipFunctionalTests=true \ + -DskipModuleFunctionalTests=false \ + -Dtests.max-container-count=5 \ + -DdockerfileName=Dockerfile-modules \ + -Dcheckstyle.skip \ + --file extra/pom.xml + + - name: Emitting run result of functional test + if: always() + uses: dorny/test-reporter@v2.5.0 + with: + name: 'Module functional tests' + working-directory: 'target/failsafe-reports' + path: 'TEST-*.xml' + reporter: java-junit + use-actions-summary: 'true' + list-suites: 'failed' + list-tests: 'failed' + fail-on-error: true + fail-on-empty: true + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-asset-publish.yml b/.github/workflows/release-asset-publish.yml index 1de13751c3a..bfa938bebe9 100644 --- a/.github/workflows/release-asset-publish.yml +++ b/.github/workflows/release-asset-publish.yml @@ -2,7 +2,7 @@ name: Publish release .jar on: workflow_run: - workflows: [Publish release] + workflows: [ Publish release ] types: - completed @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ 17 ] + java: [ 21 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: 'temurin' cache: 'maven' diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index b34d4827eae..75ea23441de 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,27 +2,25 @@ name: Publish release on: push: - branches: - - master + tags: + - '*' + +permissions: + contents: read jobs: update_release_draft: name: Publish release with notes + permissions: + contents: write 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 + uses: release-drafter/release-drafter@v6 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/.github/workflows/scripts/codepath-notification b/.github/workflows/scripts/codepath-notification new file mode 100644 index 00000000000..371c86fc652 --- /dev/null +++ b/.github/workflows/scripts/codepath-notification @@ -0,0 +1,26 @@ +# when a changed file paths matches the regex, send an alert email +# structure of the file is: +# +# javascriptRegex : email address +# +# For example, in PBS Java, there are many paths that can belong to bid adapter: +# +# /src/main/java/org/prebid/server/bidder/BIDDER +# /src/main/resources/static/bidder-params/BIDDER.json +# /src/main/resources/bidder-config/BIDDER.yaml +# /src//main/java/org/prebid/server/proto/openrtb/ext/request/BIDDER +# /src/test/resources/org/prebid/server/it/openrtb2/BIDDER +# /src/test/java/org/prebid/server/it/BIDDERTest.java +# /src/test/java/org/prebid/server/bidder/BIDDER +# /src/main/java/org/prebid/server/spring/config/bidder/BIDDERConfiguration.java +# +# The aim is to find a minimal set of regex patterns that matches any file in these paths + +/ix|Ix|ix.json|ix.yaml: pdu-supply-prebid@indexexchange.com +appnexus|Appnexus: prebid@microsoft.com +pubmatic|Pubmatic: header-bidding@pubmatic.com +openx|OpenX: prebid@openx.com +medianet|Medianet: prebid@media.net +thetradedesk|TheTradeDesk: Prebid-Maintainers@thetradedesk.com +gumgum|GumGum: prebid@gumgum.com +kargo|Kargo: kraken@kargo.com diff --git a/.github/workflows/scripts/send-notification-on-change.js b/.github/workflows/scripts/send-notification-on-change.js new file mode 100644 index 00000000000..f4e4fdcd3ca --- /dev/null +++ b/.github/workflows/scripts/send-notification-on-change.js @@ -0,0 +1,139 @@ +// send-notification-on-change.js +// +// called by the code-path-changes.yml workflow, this script queries github for +// the changes in the current PR, checkes the config file for whether any of those +// file paths are set to alert an email address, and sends email to multiple +// parties if needed + +const fs = require('fs'); +const path = require('path'); +const axios = require('axios'); +const nodemailer = require('nodemailer'); + +async function getAccessToken(clientId, clientSecret, refreshToken) { + try { + const response = await axios.post('https://oauth2.googleapis.com/token', { + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: 'refresh_token', + }); + return response.data.access_token; + } catch (error) { + console.error('Failed to fetch access token:', error.response?.data || error.message); + process.exit(1); + } +} + +(async () => { + const configFilePath = path.join(__dirname, 'codepath-notification'); + const repo = process.env.GITHUB_REPOSITORY; + const prNumber = process.env.GITHUB_PR_NUMBER; + const token = process.env.GITHUB_TOKEN; + + // Generate OAuth2 access token + const clientId = process.env.OAUTH2_CLIENT_ID; + const clientSecret = process.env.OAUTH2_CLIENT_SECRET; + const refreshToken = process.env.OAUTH2_REFRESH_TOKEN; + + // validate params + if (!repo || !prNumber || !token || !clientId || !clientSecret || !refreshToken) { + console.error('Missing required environment variables.'); + process.exit(1); + } + + // the whole process is in a big try/catch. e.g. if the config file doesn't exist, github is down, etc. + try { + // Read and process the configuration file + const configFileContent = fs.readFileSync(configFilePath, 'utf-8'); + const configRules = configFileContent + .split('\n') + .filter(line => line.trim() !== '' && !line.trim().startsWith('#')) // Ignore empty lines and comments + .map(line => { + const [regex, email] = line.split(':').map(part => part.trim()); + return { regex: new RegExp(regex), email }; + }); + + // Fetch changed files from github + const [owner, repoName] = repo.split('/'); + const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/pulls/${prNumber}/files`; + const response = await axios.get(apiUrl, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + }); + + const changedFiles = response.data.map(file => file.filename); + console.log('Changed files:', changedFiles); + + // match file pathnames that are in the config and group them by email address + const matchesByEmail = {}; + changedFiles.forEach(file => { + configRules.forEach(rule => { + if (rule.regex.test(file)) { + if (!matchesByEmail[rule.email]) { + matchesByEmail[rule.email] = []; + } + matchesByEmail[rule.email].push(file); + } + }); + }); + + // Exit successfully if no matches were found + if (Object.keys(matchesByEmail).length === 0) { + console.log('No matches found. Exiting successfully.'); + process.exit(0); + } + + console.log('Grouped matches by email:', matchesByEmail); + + // get ready to email the changes + const accessToken = await getAccessToken(clientId, clientSecret, refreshToken); + + // Configure Nodemailer with OAuth2 + // service: 'Gmail', + const transporter = nodemailer.createTransport({ + host: "smtp.gmail.com", + port: 465, + secure: true, + auth: { + type: 'OAuth2', + user: 'info@prebid.org', + clientId: clientId, + clientSecret: clientSecret, + refreshToken: refreshToken, + accessToken: accessToken + }, + }); + + // Send one email per recipient + for (const [email, files] of Object.entries(matchesByEmail)) { + const emailBody = ` + ${email}, +

+ Files owned by you have been changed in open source ${repo}. The pull request is #${prNumber}. These are the files you own that have been modified: +

+ `; + + try { + await transporter.sendMail({ + from: `"Prebid Info" `, + to: email, + subject: `Files have been changed in open source ${repo}`, + html: emailBody, + }); + + console.log(`Email sent successfully to ${email}`); + console.log(`${emailBody}`); + } catch (error) { + console.error(`Failed to send email to ${email}:`, error.message); + } + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +})(); diff --git a/.github/workflows/trivy-security-check.yml b/.github/workflows/trivy-security-check.yml index 044b7e39af6..b73eda3b40d 100644 --- a/.github/workflows/trivy-security-check.yml +++ b/.github/workflows/trivy-security-check.yml @@ -1,27 +1,35 @@ -name: Security Check +name: Trivy Security Scan on: pull_request: - branches: [master] + branches: [ 'master' ] + schedule: + - cron: '0 3 * * 1' + +permissions: + contents: read jobs: build: name: Trivy security check + permissions: + security-events: write runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@0.33.1 with: scan-type: 'fs' + scan-ref: '.' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: 'trivy-results.sarif' 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/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index bf82ff01c6c..00000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index dc3affce3dd..d58dfb70bab 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -6,7 +6,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -14,5 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Dockerfile b/Dockerfile index d69d5346506..7de0126d535 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM amazoncorretto:17 +FROM amazoncorretto:21.0.8-al2023 WORKDIR /app/prebid-server diff --git a/Dockerfile-modules b/Dockerfile-modules index a9cbfe71b31..1626999164a 100644 --- a/Dockerfile-modules +++ b/Dockerfile-modules @@ -1,4 +1,4 @@ -FROM amazoncorretto:17 +FROM amazoncorretto:21.0.8-al2023 WORKDIR /app/prebid-server diff --git a/README.md b/README.md index 44d1ffbd92b..b5aa6539aae 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Follow next steps to create JAR file which can be deployed locally. - Install prerequsites - Java SDK: Oracle's or Corretto. Let us know if there's a distribution PBS-Java doesn't work with. + - Java SDK Version: 21 - Maven - Clone the project: @@ -73,8 +74,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 +84,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 +101,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..bf89c1c3d83 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -11,6 +11,8 @@ 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.impression-limit` - a max number of impressions allowed for the auction, impressions that exceed this limit will be dropped, 0 means no limit. - `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 @@ -18,8 +20,29 @@ There are two ways to configure application settings: database and file. This do operational warning. - "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.bidadjustments` - configuration JSON for default bid adjustments +- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{, *}.{, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, and (`*` means ANY) +- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static) +- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment +- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment - `auction.events.enabled` - enables events for account if true -- `auction.debug-allow` - enables debug output in the auction response. Default `true`. +- `auction.bid-rounding` - bid rounding options are: + - **down** - rounding down to the lower price bucket + - **up** - rounding up to the higher price bucket + - **timesplit** - 50% of the time rounding down to the lower PB and 50% of the time rounding up to the higher price bucket + - **true** - if the price >= 50% of the range, rounding up to the higher price bucket, otherwise rounding down +- `auction.price-floors.enabled` - 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 +53,63 @@ 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 +- `auction.cache.enabled` - enables bids caching for account if true. Defaults to true. - `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 +- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the 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 +273,59 @@ 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: # optional + secretAccessKey: #optional + 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 +``` + +If `accessKeyId` and `secretAccessKey` are not specified in the Prebid Server configuration then AWS credentials will be looked up in this order: +- Java System Properties - `aws.accessKeyId` and `aws.secretAccessKey` +- Environment Variables - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` +- Web Identity Token credentials from system properties or environment variables +- Credential profiles file at the default location (`~/.aws/credentials`) shared by all AWS SDKs and the AWS CLI +- Credentials delivered through the Amazon EC2 container service if "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" environment variable is set and security manager has permission to access the variable, +- Instance profile credentials delivered through the Amazon EC2 metadata service + +### 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/build.md b/docs/build.md index 67b0b8af26e..ed2c18b7e0a 100644 --- a/docs/build.md +++ b/docs/build.md @@ -1,9 +1,15 @@ # Build project To build the project, you will need at least -[Java 11](https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz) +[Java 21](https://whichjdk.com/) and [Maven](https://maven.apache.org/) installed. +If for whatever reason this Java reference will be stale, +you can always get the current project Java version from `pom.xml` property +```xml +... +``` + To verify the installed Java run in console: ```bash @@ -13,9 +19,9 @@ java -version which should show something like (yours may be different): ``` -openjdk version "11.0.2" 2019-01-15 -OpenJDK Runtime Environment 18.9 (build 11.0.2+9) -OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode) +openjdk version "21.0.5" 2024-10-15 LTS +OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS) +OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing) ``` Follow next steps to create JAR which can be deployed locally. diff --git a/docs/config-app.md b/docs/config-app.md index 79cbf9f85ef..a661f5a74a2 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -14,18 +14,24 @@ This section can be extended against standard [Spring configuration](https://doc This parameter exists to allow to change the location of the directory Vert.x will create because it will and there is no way to make it not. - `vertx.init-timeout-ms` - time to wait for asynchronous initialization steps completion before considering them stuck. When exceeded - exception is thrown and Prebid Server stops. - `vertx.enable-per-client-endpoint-metrics` - enables HTTP client metrics per destination endpoint (`host:port`) +- `vertx.round-robin-inet-address` - enables round-robin inet address selection of the ip address to use ## Server - `server.max-headers-size` - set the maximum length of all headers. - `server.ssl` - enable SSL/TLS support. - `server.jks-path` - path to the java keystore (if ssl is enabled). - `server.jks-password` - password for the keystore (if ssl is enabled). +- `server.cpu-load-monitoring.measurement-interval-ms` - the CPU load monitoring interval (milliseconds) ## 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.max-initial-line-length` - set the maximum length of the initial line +- `server.idle-timeout` - set the maximum time idle connections could exist before being reaped +- `server.enable-quickack` - enables the TCP_QUICKACK option - only with linux native transport. +- `server.enable-reuseport` - set the value of reuse port - `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 +67,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 +85,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. @@ -88,10 +97,12 @@ Removes and downloads file again if depending service cant process probably corr - `auction.cache.expected-request-time-ms` - approximate value in milliseconds for Cache Service interacting. - `auction.cache.only-winning-bids` - if equals to `true` only the winning bids would be cached. Has lower priority than request-specific flags. - `auction.generate-bid-id` - whether to generate seatbid[].bid[].ext.prebid.bidid in the OpenRTB response. +- `auction.enforce-random-bid-id` - whether to enforce generating a robust random seatbid[].bid[].id in the OpenRTB response if the initial value is less than 17 characters. - `auction.validations.banner-creative-max-size` - enables creative max size validation for banners. Possible values: `skip`, `enforce`, `warn`. Default is `skip`. - `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 @@ -103,14 +114,15 @@ Removes and downloads file again if depending service cant process probably corr - `auction.timeout-notification.log-sampling-rate` - instructs apply sampling when logging bidder timeout notification results ## Video -- `auction.video.stored-required` - flag forces to merge with stored request -- `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs. +- `video.stored-request-required` - flag forces to merge with stored request - `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.blocklisted-accounts` - comma separated list of blocklisted account IDs. - `auction.video.escape-log-cache-regex` - regex to remove from cache debug log xml. +- `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. ## Setuid - `setuid.default-timeout-ms` - default operation timeout for requests to `/setuid` endpoint. +- `setuid.number-of-uid-cookies` - specifies the maximum number of UID cookies that can be returned in the `/setuid` endpoint response. If it's not specified `1` will be taken as the default value. ## Cookie Sync - `cookie-sync.default-timeout-ms` - default operation timeout for requests to `/cookie_sync` endpoint. @@ -122,6 +134,7 @@ Removes and downloads file again if depending service cant process probably corr ## Vtrack - `vtrack.allow-unknown-bidder` - flag that allows servicing requests with bidders who were not configured in Prebid Server. - `vtrack.modify-vast-for-unknown-bidder` - flag that allows modifying the VAST value and adding the impression tag to it, for bidders who were not configured in Prebid Server. +- `vtrack.default-timeout-ms` - a default timeout in ms for the vtrack request ## Adapters - `adapters.*` - the section for bidder specific configuration options. @@ -143,6 +156,7 @@ There are several typical keys: - `adapters..usersync.type` - usersync type (i.e. redirect, iframe). - `adapters..usersync.support-cors` - flag signals if CORS supported by usersync. - `adapters..debug.allow` - enables debug output in the auction response for the given bidder. Default `true`. +- `adapters..tmax-deduction-ms` - adjusts the tmax sent to the bidder by deducting the provided value (ms). Default `0 ms` - no deduction. In addition, each bidder could have arbitrary aliases configured that will look and act very much the same as the bidder itself. Aliases are configured by adding child configuration object at `adapters..aliases..`, aliases @@ -156,9 +170,8 @@ Also, each bidder could have its own bidder-specific options. ## Logging - `logging.http-interaction.max-limit` - maximum value for the number of interactions to log in one take. - -## Logging - `logging.change-level.max-duration-ms` - maximum duration (in milliseconds) for which logging level could be changed. +- `logging.sampling-rate` - a percentage of messages that are logged ## Currency Converter - `currency-converter.external-rates.enabled` - if equals to `true` the currency conversion service will be enabled to fetch updated rates and convert bid currencies from external source. Also enables `/currency-rates` endpoint on admin port. @@ -202,32 +215,17 @@ 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.logging-changelevel.enabled` - if equals to `true` the endpoint will be available. +- `admin-endpoints.logging-changelevel.path` - the server context path where the endpoint will be accessible +- `admin-endpoints.logging-changelevel.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`. +- `admin-endpoints.logging-changelevel.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`. @@ -237,6 +235,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. @@ -274,10 +278,20 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a - `metrics.accounts.basic-verbosity` - a list of accounts for which only basic metrics will be submitted. - `metrics.accounts.detailed-verbosity` - a list of accounts for which all metrics will be submitted. +For `JVM` metrics +- `metrics.jmx.enabled` - if equals to `true` then `jvm.gc` and `jvm.memory` metrics will be submitted + ## Cache - `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`. +- `cache.internal.scheme` - set the internal Cache Service protocol: `http`, `https`, etc., the internal scheme get priority over the external one when provided. +- `cache.internal.host` - set the internal Cache Service destination in format `host:port`, the internal port get priority over the external one when provided. +- `cache.internal.path` - set the internal Cache Service path, for example `/cache`, the internal path get priority over the external one when provided. +- `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`. +- `cache.api-key-secured` - if set to `true`, will cause Prebid Server to add a special API key header to Prebid Cache requests. +- `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. @@ -285,6 +299,8 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a for particular publisher account. Overrides `cache.banner-ttl-seconds` property. - `cache.account..video-ttl-seconds` - how long (in seconds) video creative will be available in Cache Service for particular publisher account. Overrides `cache.video-ttl-seconds` property. +- `cache.default-ttl-seconds.{banner, video, audio, native}` - a default value how long (in seconds) a creative of the specific type will be available in Cache Service +- `cache.append-trace-info-to-cache-id` - if set to `true`, causes the addition account ID and datacenter to cache UUID: _ACCOUNT-DATACENTER-remainderOfUUID_. Implies that cache UUID will be generated by the Prebid Server. ## Application settings (account configuration, stored ad unit configurations, stored requests) Preconfigured application settings can be obtained from multiple data sources consequently: @@ -294,6 +310,10 @@ Preconfigured application settings can be obtained from multiple data sources co Warning! Application will not start in case of no one data source is defined and you'll get an exception in logs. +For requests validation mode available next options: +- `settings.fail-on-unknown-bidders` - fail with validation error or just make warning for unknown bidders. +- `settings.fail-on-disabled-bidders` - fail with validation error or just make warning for disabled bidders. + For filesystem data source available next options: - `settings.filesystem.settings-filename` - location of file settings. - `settings.filesystem.stored-requests-dir` - directory with stored requests. @@ -309,8 +329,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. @@ -324,9 +346,10 @@ For HTTP data source available next options: - `settings.http.amp-endpoint` - the url to fetch AMP stored requests. - `settings.http.video-endpoint` - the url to fetch video stored requests. - `settings.http.category-endpoint` - the url to fetch categories for long form video. +- `settings.http.rfc3986-compatible` - if equals to `true` the url will be build according to RFC 3986, `false` by default For account processing rules available next options: -- `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejected with 401. +- `settings.enforce-valid-account` - if equals to `true` then request without account id will be rejection with 401. - `settings.generate-storedrequest-bidrequest-id` - overrides `bidrequest.id` in amp or app stored request with generated UUID if true. Default value is false. This flag can be overridden by setting `bidrequest.id` as `{{UUID}}` placeholder directly in stored request. It is possible to specify default account configuration values that will be assumed if account config have them @@ -353,6 +376,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 +385,36 @@ 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 S3 storage configuration +- `settings.in-memory-cache.s3-update.refresh-rate` - refresh period in ms for stored request updates in S3 +- `settings.s3.access-key-id` - an access key (optional) +- `settings.s3.secret-access-key` - a secret access key (optional) +- `settings.s3.region` - a region, AWS_GLOBAL by default +- `settings.s3.endpoint` - an endpoint +- `settings.s3.bucket` - a bucket name +- `settings.s3.force-path-style` - forces the S3 client to use path-style addressing for buckets. +- `settings.s3.accounts-dir` - a directory with stored accounts +- `settings.s3.stored-imps-dir` - a directory with stored imps +- `settings.s3.stored-requests-dir` - a directory with stored requests +- `settings.s3.stored-responses-dir` - a directory with stored responses + +If `settings.s3.access-key-id` and `settings.s3.secret-access-key` are not specified in the Prebid Server configuration then AWS credentials will be looked up in this order: +- Java System Properties - `aws.accessKeyId` and `aws.secretAccessKey` +- Environment Variables - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` +- Web Identity Token credentials from system properties or environment variables +- Credential profiles file at the default location (`~/.aws/credentials`) shared by all AWS SDKs and the AWS CLI +- Credentials delivered through the Amazon EC2 container service if "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" environment variable is set and security manager has permission to access the variable, +- Instance profile credentials delivered through the Amazon EC2 metadata service + For targeting available next options: - `settings.targeting.truncate-attr-chars` - set the max length for names of targeting keywords (0 means no truncation). @@ -402,6 +448,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `gdpr.eea-countries` - comma separated list of countries in European Economic Area (EEA). - `gdpr.default-value` - determines GDPR in scope default value (if no information in request and no geolocation data). - `gdpr.host-vendor-id` - the organization running a cluster of Prebid Servers. +- `datacenter-region` - the datacenter region of a cluster of Prebid Servers - `gdpr.enabled` - gdpr feature switch. Default `true`. - `gdpr.purposes.pN.enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`. Default `full` - `gdpr.purposes.pN.enforce-vendors` - if equals to `true`, user must give consent to use vendors. Purposes will be omitted. Default `true` @@ -431,8 +478,30 @@ If not defined in config all other Health Checkers would be disabled and endpoin - `geolocation.type` - set the geo location service provider, can be `maxmind` or custom provided by hosting company. - `geolocation.maxmind` - section for [MaxMind](https://www.maxmind.com) configuration as geo location service provider. - `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration. +- `geolocation.configurations[]` - a list of geo-lookup configurations for the `configuration` `geolocation.type` +- `geolocation.configurations[].address-pattern` - an address pattern for matching an IP to look up +- `geolocation.configurations[].geo-info.continent` - a continent to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.country` - a country to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.region` - a region to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.region-code` - a region code to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.city` - a city to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.metro-google` - a metro Google to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.metro-nielsen` - a metro Nielsen to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.zip` - a zip to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.connection-speed` - a connection-speed to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.lat` - a lat to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.lon` - a lon to return on the `configuration` geo-lookup +- `geolocation.configurations[].geo-info.time-zone` - a time zone to return on the `configuration` geo-lookup + +## IPv6 +- `ipv6.always-mask-right` - a bit mask for masking an IPv6 address of the device +- `ipv6.anon-left-mask-bits` - a bit mask for anonymizing an IPv6 address of the device +- `ipv6.private-networks` - a list of known private/local networks to skip masking of an IP address of the device ## Analytics +- `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level. + +For the `pubstack` analytics adapter - `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,40 +511,29 @@ 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. +For the `greenbids` analytics adapter +- `analytics.greenbids.enabled` - if equals to `true` the Greenbids analytics module will be enabled. Default value is `false`. +- `analytics.greenbids.analytics-server-version` - a server version to add to the event +- `analytics.greenbids.analytics-server` - url for reporting events +- `analytics.greenbids.timeout-ms` - timeout in milliseconds for report requests. +- `analytics.greenbids.exploratory-sampling-split` - a sampling rate for report requests +- `analytics.greenbids.default-sampling-rate` - a default sampling rate for report requests + +For the `agma` analytics adapter +- `analytics.agma.enabled` - if equals to `true` the Agma analytics module will be enabled. Default value is `false`. +- `analytics.agma.endpoint.url` - url for reporting events +- `analytics.agma.endpoint.timeout-ms` - timeout in milliseconds for report requests. +- `analytics.agma.endpoint.gzip` - if equals to `true` the Agma analytics module enables gzip encoding. Default value is `false`. +- `analytics.agma.buffers.size-bytes` - threshold in bytes for buffer to send events. +- `analytics.agma.buffers.count` - threshold in events count for buffer to send events. +- `analytics.agma.buffers.timeout-ms` - max period between two reports. +- `analytics.agma.accounts[].code` - an account code to send with an event +- `analytics.agma.accounts[].publisher-id` - a publisher id to match an event to send +- `analytics.agma.accounts[].site-app-id` - a site or app id to match an event to send + +## Modules +- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan +- `settings.modules.require-config-to-invoke` - when enabled it requires a runtime config to exist for a module. ## Debugging - `debug.override-token` - special string token for overriding Prebid Server account and/or adapter debug information presence in the auction response. @@ -483,3 +541,20 @@ If not defined in config all other Health Checkers would be disabled and endpoin To override (force enable) account and/or bidder adapter debug setting, a client must include `x-pbs-debug-override` HTTP header in the auction call containing same token as in the `debug.override-token` property. This will make Prebid Server ignore account `auction.debug-allow` and/or `adapters..debug.allow` properties. + +## Privacy Sandbox +- `auction.privacysandbox.topicsdomain` - the list of Sec-Browsing-Topics for the Privacy Sandbox + +## AMP +- `amp.custom-targeting` - a list of bidders that support custom targeting + +## Hooks +- `hooks.host-execution-plan` - a host execution plan for modules +- `hooks.default-account-execution-plan` - a default account execution plan + +## Price Floors Debug +- `price-floors.enabled` - enables price floors for account if true. Defaults to true. +- `price-floors.min-max-age-sec` - a price floors fetch data time to live in cache. +- `price-floors.min-period-sec` - a refresh period for fetching price floors data. +- `price-floors.min-timeout-ms` - a min timeout in ms for fetching price floors data. +- `price-floors.max-timeout-ms` - a max timeout in ms for fetching price floors data. 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/bid-adapter-porting-guide.md b/docs/developers/bid-adapter-porting-guide.md new file mode 100644 index 00000000000..ba0c900fdfb --- /dev/null +++ b/docs/developers/bid-adapter-porting-guide.md @@ -0,0 +1,72 @@ +# Porting Guide + +## Overview + +First, thank you for taking on the migration of an adapter from Go to Java. But really the best way to think of it is not as straight port. Instead, we recommend treat this task as a re-implementation. It will take a few adapters before you fully get the hang of it, and that's okay—everyone goes through a learning curve. + +Keep in mind that the PBS-Go team is more lenient about what they allow in adapters compared to the PBS-Java team. + +## Pull Request Requirements + +We would appreciate it if your porting PR title follows these patterns: + +- `Port : New Adapter` – For porting a completely new adapter to the project (e.g., `Port Kobler: New Adapter`). +- `Port : ` – For porting a specific update to an existing adapter (e.g., `Port OpenX: Native Support`). +- `Port : New alias for ` – For porting an alias of an existing adapter to the project (e.g., `Port Artechnology: New alias of StartHub`). + +Additionally, we kindly ask that you: + +- Link any existing GitHub issues that your PR resolves. This ensures the issue will be automatically closed when your PR is merged. +- Add the label `do not port` to your PR. + +## Porting Requirements + +1. **Feature Parity**: A Java adapter should have the same functionality as the Go adapter. +2. **Java Adapter Code Should:** + - Follow the code style of the PBS-Java repository (see [the code style page](code-style.md)). + - Be well-written Java code: clear, readable, optimized, and following best practices. + - Maintain a structure similar to existing adapters (see below). +3. **The adapter should be covered with tests:** + - Unit tests for implementation details. + - A simple integration test to ensure the adapter is reachable, can send requests to the bidder, and that its configuration works. + +### What does "having a similar structure to existing adapters" mean? + +The PBS-Java codebase has evolved over time. While existing adapters may not be perfect and could contain legacy issues (e.g., using outdated Java syntax), they still serve as a valuable reference for learning, inspiration, and even reuse. + +Each adapter is unique, but most share common patterns. For example, nearly every adapter includes: + +1. **A `makeHttpRequests(...)` method** + - Iterates over the `imps` in the bid request: + - Parses `imp[].ext.prebid.bidder` (i.e., bidder static parameters). + - Modifies the `imp`. + - Collects errors encountered during `imp` processing. + - Prepares outgoing request(s): + - Constructs headers. + - Builds the request URL. + - Modifies the incoming bid request based on the updated `imps`. + +2. **A `makeBids(...)` method** + - Parses the `BidResponse`. + - Iterates over `seatBids` and `bids`. + - Creates a list of `BidderBid` objects. + +### Ensuring Structural Consistency + +To maintain consistency across adapters: +- Fit the Go adapter functionality into the Java adapter structure. +- Use the same or similar method and variable names where applicable. +- Reuse existing solutions for common functionality (e.g., use `BidderUtil`, `HttpUtil` classes). +- Ensure unit tests follow a similar structure, with comparable test cases and code patterns. + +## Specific Rules and Tips for Porting + +1. Begin by determining how the Go adapter's functionality fits into the Java adapter structure. +2. Go adapters deserialize JSON objects in-place, while Java adapters work with pre-deserialized objects. As a result, many errors thrown in the Go version do not apply in Java. +3. **No hardcoded URLs.** If an adapter has a "test URL," it must be defined in the YAML file. See `org.prebid.server.spring.config.bidder.NextMillenniumConfiguration.NextMillenniumConfigurationProperties` for an example of how to handle special YAML entries. +4. The structure of Go and Java bidder configuration files differs—do not copy and paste directly. Pay attention to details such as macros in the endpoint and redirect/iframe URLs. +5. **Prohibited in bidder adapters:** + - Blocking code. + - Fully dynamic hostnames in URLs. + - Non-thread-safe code (bidder adapters should not store state internally). +6. If an adapter has no special logic, consider using an alias to `Generic` instead. In this case, there will still need to be an integration test for this bidder. e.g. `src/test/java/org/prebid/server/it/BidderNameTest.java` diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md index 78728fef18a..ba7fb0ee526 100644 --- a/docs/developers/code-reviews.md +++ b/docs/developers/code-reviews.md @@ -3,33 +3,21 @@ ## Standards Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls). -All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge. - -Very small pull requests may be merged with just one review if they: - -1. Do not change the public API. -2. Have low risk of bugs, in the opinion of the reviewer. -3. Introduce no new features, or impact the code architecture. - -Larger pull requests must meet at least one of the following two additional requirements. - -1. Have a second approval from a core member -2. Be open for 5 business days with no new changes requested. +1. PRs that touch only adapters and modules can be approved by one reviewer before merge. +2. PRs that touch PBS-core must be reviewed and approved by at least two 'core' reviewers before merge. ## Process -New pull requests should be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) -to a core member for review within 3 business days of being opened. -That person should either approve the changes or request changes within 4 business days of being assigned. -If they're too busy, they should assign it to someone else who can review it within that timeframe. +New pull requests must be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) +to a reviewer within 5 business days of being opened. That person must either approve the changes or request changes within 5 business days of being assigned. + +If a reviewer is too busy, they should re-assign it to someone else as soon as possible so that person has enough time to take over the review and still meet the 5-day goal. Please tag the new reviewer in the PR. If you don't know who to assign it to, use the #prebid-server-java-dev Slack channel to ask for help in re-assigning. -If the changes are small, that member can merge the PR once the changes are complete. Otherwise, they should -assign the pull request to another member for a second review. +If a reviewer is going to be unavailable for more than a few days, they should update the notes column of the duty spreadsheet or drop a note about their availability into the Slack channel. -The pull request can then be merged whenever the second reviewer approves, or if 5 business days pass with no farther -changes requested by anybody, whichever comes first. +After the review, if the PR touches PBS-core, it must be assigned to a second reviewer. -## Priorities +## Review Priorities Code reviews should focus on things which cannot be validated by machines. @@ -43,3 +31,10 @@ 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. +- Specific bid adapter rules: + - The email contact must work and be a group, not an individual. + - Host endpoints cannot be fully dynamic. i.e. they can utilize "https://REGION.example.com", but not "https://HOST". + - They cannot _require_ a "region" parameter. Region may be an optional parameter, but must have a default. + - 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* diff --git a/docs/developers/code-style.md b/docs/developers/code-style.md index 14704d20799..de42811030f 100644 --- a/docs/developers/code-style.md +++ b/docs/developers/code-style.md @@ -28,7 +28,7 @@ in `pom.xml` directly. It is recommended to define version of library to separate property in `pom.xml`: -``` +```xml 2.6.2 @@ -48,7 +48,7 @@ It is recommended to define version of library to separate property in `pom.xml` Do not use wildcard in imports because they hide what exactly is required by the class. -``` +```java // bad import java.util.*; @@ -61,7 +61,7 @@ import java.util.Map; Prefer to use `camelCase` naming convention for variables and methods. -``` +```java // bad String account_id = "id"; @@ -71,7 +71,7 @@ String accountId = "id"; Name of variable should be self-explanatory: -``` +```java // bad String s = resolveParamA(); @@ -83,7 +83,7 @@ This helps other developers flesh your code out better without additional questi For `Map`s it is recommended to use `To` between key and value designation: -``` +```java // bad Map map = getData(); @@ -97,7 +97,7 @@ Make data transfer object(DTO) classes immutable with static constructor. This can be achieved by using Lombok and `@Value(staticConstructor="of")`. When constructor uses multiple(more than 4) arguments, use builder instead(`@Builder`). If dto must be modified somewhere, use builders annotation `toBuilder=true` parameter and rebuild instance by calling `toBuilder()` method. -``` +```java // bad public class MyDto { @@ -138,7 +138,7 @@ final MyDto updatedDto = myDto.toBuilder().value("newValue").build(); Although Java supports the `var` keyword at the time of writing this documentation, the maintainers have chosen not to utilize it within the PBS codebase. Instead, write full variable type. -``` +```java // bad final var result = getResult(); @@ -150,7 +150,7 @@ final Data result = getResult(); Enclosing parenthesis should be placed on expression end. -``` +```java // bad methodCall( long list of arguments @@ -163,7 +163,7 @@ methodCall( This also applies for nested expressions. -``` +```java // bad methodCall( nestedCall( @@ -181,7 +181,7 @@ methodCall( Please, place methods inside a class in call order. -``` +```java // bad public interface Test { @@ -249,7 +249,7 @@ Define interface first method, then all methods that it is calling, then second Not strict, but methods with long parameters list, that cannot be placed on single line, should add empty line before body definition. -``` +```java // bad public static void method( parameters definitions) { @@ -266,7 +266,7 @@ public static void method( Use collection literals where it is possible to define and initialize collections. -``` +```java // bad final List foo = new ArrayList(); foo.add("foo"); @@ -278,7 +278,7 @@ final List foo = List.of("foo", "bar"); Also, use special methods of Collections class for empty or single-value one-line collection creation. This makes developer intention clear and code less error-prone. -``` +```java // bad return List.of(); @@ -296,7 +296,7 @@ return Collections.singletonList("foo"); It is recommended to declare variable as `final`- not strict but rather project convention to keep the code safe. -``` +```java // bad String value = "value"; @@ -308,7 +308,7 @@ final String value = "value"; Results of long ternary operators should be on separate lines: -``` +```java // bad boolean result = someVeryVeryLongConditionThatForcesLineWrap ? firstResult : secondResult; @@ -321,7 +321,7 @@ boolean result = someVeryVeryLongConditionThatForcesLineWrap Not so strict, but short ternary operations should be on one line: -``` +```java // bad boolean result = someShortCondition ? firstResult @@ -335,7 +335,7 @@ boolean result = someShortCondition ? firstResult : secondResult; Do not rely on operator precedence in boolean logic, use parenthesis instead. This will make code simpler and less error-prone. -``` +```java // bad final boolean result = a && b || c; @@ -347,7 +347,7 @@ final boolean result = (a && b) || c; Try to avoid hard-readable multiple nested method calls: -``` +```java // bad int resolvedValue = resolveValue(fetchExternalJson(url, httpClient), populateAdditionalKeys(mainKeys, keyResolver)); @@ -361,7 +361,7 @@ int resolvedValue = resolveValue(externalJson, additionalKeys); Try not to retrieve same data more than once: -``` +```java // bad if (getData() != null) { final Data resolvedData = resolveData(getData()); @@ -380,7 +380,7 @@ if (data != null) { If you're dealing with incoming data, please be sure to check if the nested object is not null before chaining. -``` +```java // bad final ExtRequestTargeting targeting = bidRequest.getExt().getPrebid().getTargeting(); @@ -400,7 +400,7 @@ We are trying to get rid of long chains of null checks, which are described in s Don't leave commented code (don't think about the future). -``` +```java // bad // String iWillUseThisLater = "never"; ``` @@ -426,7 +426,7 @@ The code should be covered over 90%. The common way for writing tests has to comply with `given-when-then` style. -``` +```java // given final BidRequest bidRequest = BidRequest.builder().id("").build(); @@ -451,7 +451,7 @@ The team decided to use name `target` for class instance under test. Unit tests should be as granular as possible. Try to split unit tests into smaller ones until this is impossible to do. -``` +```java // bad @Test public void testFooBar() { @@ -487,7 +487,7 @@ public void testBar() { This also applies to cases where same method is tested with different arguments inside single unit test. Note: This represents the replacement we have selected for parameterized testing. -``` +```java // bad @Test public void testFooFirstSecond() { @@ -527,7 +527,7 @@ It is also recommended to structure test method names with this scheme: name of method that is being tested, word `should`, what a method should return. If a method should return something based on a certain condition, add word `when` and description of a condition. -``` +```java // bad @Test public void doSomethingTest() { @@ -547,7 +547,7 @@ public void processDataShouldReturnResultWhenInputIsData() { Place data used in test as close as possible to test code. This will make tests easier to read, review and understand. -``` +```java // bad @Test public void testFoo() { @@ -576,7 +576,7 @@ This point also implies the next one. Since we are trying to improve test simplicity and readability and place test data close to tests, we decided to avoid usage of top level constants where it is possible. Instead, just inline constant values. -``` +```java // bad public class TestClass { @@ -609,7 +609,7 @@ public class TestClass { Don't use real information in tests, like existing endpoint URLs, account IDs, etc. -``` +```java // bad String ENDPOINT_URL = "https://prebid.org"; 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..c07e0660598 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 @@ -37,6 +37,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul. ## General auction metrics - `app_requests` - number of requests received from applications +- `debug_requests` - number of requests received (when debug mode is enabled) - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID - `request_time` - timer tracking how long did it take for Prebid Server to serve a request - `imps_requested` - number if impressions requested @@ -44,7 +45,9 @@ 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 +- `disabled_bidder` - number of disabled bidders received within requests +- `unknown_bidder` - number of unknown bidders received within requests +- `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 @@ -89,7 +92,10 @@ Following metrics are collected and submitted if account is configured with `bas Following metrics are collected and submitted if account is configured with `detailed` verbosity: - `account..requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `` broken down by type of incoming request -- `account..requests.rejected` - number of rejected requests caused by incorrect `accountId` +- `account..debug_requests` - number of requests received from account with `` broken down by type of incoming request (when debug mode is enabled) +- `account..requests.rejection` - number of rejection requests caused by incorrect `accountId` +- `account..requests.disabled_bidder` - number of disabled bidders received within requests from account with `` +- `account..requests.unknown_bidder` - number of unknown bidder names received within requests from account with `` - `account..adapter..request_time` - timer tracking how long did it take to make a request to `` when incoming request was from `` - `account..adapter..bids_received` - number of bids received from `` when incoming request was from `` - `account..adapter..requests.(gotbids|nobid)` - number of requests made to `` broken down by result status when incoming request was from `` @@ -98,11 +104,13 @@ Following metrics are collected and submitted if account is configured with `det - `prebid_cache.requests.ok` - timer tracking how long did successful cache requests take - `prebid_cache.requests.err` - timer tracking how long did failed cache requests take - `prebid_cache.creative_size.` - histogram tracking creative sizes for specific type +- `prebid_cache.creative_ttl.` - histogram tracking creative TTL for specific type ## Prebid Cache per-account metrics - `account..prebid_cache.requests.ok` - timer tracking how long did successful cache requests take when incoming request was from `` - `account..prebid_cache.requests.err` - timer tracking how long did failed cache requests take when incoming request was from `` - `account..prebid_cache.creative_size.` - histogram tracking creative sizes for specific type when incoming request was from `` +- `account..prebid_cache.creative_ttl.` - histogram tracking creative TTL for specific type when incoming request was from `` ## /cookie_sync endpoint metrics - `cookie_sync_requests` - number of requests received @@ -132,30 +140,16 @@ Following metrics are collected and submitted if account is configured with `det - `analytics..(auction|amp|video|cookie_sync|event|setuid).ok` - number of succeeded processed event requests - `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. +- `analytics..(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejection with bad input cause + +## Modules metrics +- `modules.module..stage..hook..call` - number of times the hook is called +- `modules.module..stage..hook..duration` - timer tracking the called hook execution time +- `modules.module..stage..hook..success.(noop|update|reject|no-invocation)` - number of times the hook is called successfully with the action applied +- `modules.module..stage..hook..(failure|timeout|execution-error)` - number of times the hook execution is failed + +## Modules per-account metrics +- `account..modules.module..call` - number of times the module is called +- `account..modules.module..duration` - timer tracking the called module execution time +- `account..modules.module..success.(noop|update|reject|no-invocation)` - number of times the module is called successfully with the action applied +- `account..modules.module..failure` - number of times the module execution is failed diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index b48eaf7780d..5d44255ca2d 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.39.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,41 @@ pb-richmedia-filter ${project.version} + + org.prebid.server.hooks.modules + pb-response-correction + ${project.version} + + + org.prebid.server.hooks.modules + greenbids-real-time-data + ${project.version} + + + org.prebid.server.hooks.modules + pb-request-correction + ${project.version} + + + org.prebid.server.hooks.modules + optable-targeting + ${project.version} + + + org.prebid.server.hooks.modules + wurfl-devicedetection + ${project.version} + + + org.prebid.server.hooks.modules + live-intent-omni-channel-identity + ${project.version} + + + org.prebid.server.hooks.modules + pb-rule-engine + ${project.version} + diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml index e04ca09ea57..1d86482129b 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.39.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/config/ConfiantAdQualityModuleConfiguration.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/config/ConfiantAdQualityModuleConfiguration.java index 7978153c34a..37ed7c9ec10 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/config/ConfiantAdQualityModuleConfiguration.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/config/ConfiantAdQualityModuleConfiguration.java @@ -34,7 +34,8 @@ public class ConfiantAdQualityModuleConfiguration { ConfiantAdQualityModule confiantAdQualityModule( @Value("${hooks.modules.confiant-ad-quality.api-key}") String apiKey, @Value("${hooks.modules.confiant-ad-quality.scan-state-check-interval}") int scanStateCheckInterval, - @Value("${hooks.modules.confiant-ad-quality.bidders-to-exclude-from-scan}") List biddersToExcludeFromScan, + @Value("${hooks.modules.confiant-ad-quality.bidders-to-exclude-from-scan}") + List biddersToExcludeFromScan, RedisConfig redisConfig, RedisRetryConfig retryConfig, Vertx vertx, @@ -43,13 +44,24 @@ ConfiantAdQualityModule confiantAdQualityModule( final RedisConnectionConfig writeNodeConfig = redisConfig.getWriteNode(); final RedisClient writeRedisNode = new RedisClient( - vertx, writeNodeConfig.getHost(), writeNodeConfig.getPort(), writeNodeConfig.getPassword(), retryConfig, "write node"); + vertx, + writeNodeConfig.getHost(), + writeNodeConfig.getPort(), + writeNodeConfig.getPassword(), + retryConfig, + "write node"); final RedisConnectionConfig readNodeConfig = redisConfig.getReadNode(); final RedisClient readRedisNode = new RedisClient( - vertx, readNodeConfig.getHost(), readNodeConfig.getPort(), readNodeConfig.getPassword(), retryConfig, "read node"); + vertx, + readNodeConfig.getHost(), + readNodeConfig.getPort(), + readNodeConfig.getPassword(), + retryConfig, + "read node"); final BidsScanner bidsScanner = new BidsScanner(writeRedisNode, readRedisNode, apiKey, objectMapper); - final RedisScanStateChecker redisScanStateChecker = new RedisScanStateChecker(bidsScanner, scanStateCheckInterval, vertx); + final RedisScanStateChecker redisScanStateChecker = new RedisScanStateChecker( + bidsScanner, scanStateCheckInterval, vertx); final Promise scannerPromise = Promise.promise(); scannerPromise.future().onComplete(r -> redisScanStateChecker.run()); diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java index 57eac3d3620..47c73e0077c 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java @@ -3,10 +3,10 @@ import com.iab.openrtb.response.Bid; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.analytics.AppliedTo; import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; @@ -24,6 +24,9 @@ public class AnalyticsMapper { private static final String INSPECTED_HAS_ISSUE = "inspected-has-issue"; private static final String INSPECTED_NO_ISSUES = "inspected-no-issues"; + private AnalyticsMapper() { + } + public static Tags toAnalyticsTags(List bidderResponsesWithIssues, List bidderResponsesWithoutIssues, List bidderResponsesNotScanned) { @@ -31,7 +34,10 @@ public static Tags toAnalyticsTags(List bidderResponsesWithIssue return TagsImpl.of(Collections.singletonList(ActivityImpl.of( AD_QUALITY_SCAN, SUCCESS_STATUS, - toActivityResults(bidderResponsesWithIssues, bidderResponsesWithoutIssues, bidderResponsesNotScanned)))); + toActivityResults( + bidderResponsesWithIssues, + bidderResponsesWithoutIssues, + bidderResponsesNotScanned)))); } private static List toActivityResults(List bidderResponsesWithIssues, diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapper.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapper.java index cf4f3557862..094b7e6b494 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapper.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapper.java @@ -13,16 +13,19 @@ public class BidsMapper { - public static RedisBidsData toRedisBidsFromBidResponses( - BidRequest bidRequest, - List bidderResponses) { + private BidsMapper() { + } + + public static RedisBidsData toRedisBidsFromBidResponses(BidRequest bidRequest, + List bidderResponses) { - final List confiantBidResponses = bidderResponses - .stream().map(bidResponse -> RedisBidResponseData + final List confiantBidResponses = bidderResponses.stream() + .map(bidResponse -> RedisBidResponseData .builder() .dspId(bidResponse.getBidder()) .bidresponse(toBidResponseFromBidderResponse(bidRequest, bidResponse)) - .build()).toList(); + .build()) + .toList(); return RedisBidsData.builder() .breq(bidRequest) @@ -30,13 +33,12 @@ public static RedisBidsData toRedisBidsFromBidResponses( .build(); } - private static BidResponse toBidResponseFromBidderResponse( - BidRequest bidRequest, - BidderResponse bidderResponse) { + private static BidResponse toBidResponseFromBidderResponse(BidRequest bidRequest, + BidderResponse bidderResponse) { return BidResponse.builder() .id(bidRequest.getId()) - .cur(bidRequest.getCur().get(0)) + .cur(bidRequest.getCur().getFirst()) .seatbid(Collections.singletonList(SeatBid.builder() .bid(bidderResponse.getSeatBid().getBids().stream().map(BidderBid::getBid).toList()) .build())) diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanner.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanner.java index d8b9657e22d..1b3afe3092d 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanner.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanner.java @@ -59,13 +59,18 @@ public Future submitBids(RedisBidsData bids) { final RedisAPI readRedisNodeAPI = this.readRedisNode.getRedisAPI(); final boolean shouldSubmit = !isScanDisabled - && readRedisNodeAPI != null && bids.getBresps().size() > 0; + && readRedisNodeAPI != null && !bids.getBresps().isEmpty(); if (shouldSubmit) { readRedisNodeAPI.get("function_submit_bids", submitHash -> { final Object submitHashResult = submitHash.result(); if (submitHashResult != null) { - final List readArgs = List.of(submitHashResult.toString(), "0", toBidsAsJson(bids), apiKey, "true"); + final List readArgs = List.of( + submitHashResult.toString(), + "0", + toBidsAsJson(bids), + apiKey, + "true"); readRedisNodeAPI.evalsha(readArgs, response -> { if (response.result() != null) { @@ -120,7 +125,7 @@ public Future isScanDisabledFlag() { if (redisAPI != null) { redisAPI.get("scan-disabled", scanDisabledValue -> { final Response scanDisabled = scanDisabledValue.result(); - isDisabled.complete(scanDisabled != null && scanDisabled.toString().equals("true")); + isDisabled.complete(scanDisabled != null && "true".equals(scanDisabled.toString())); }); return isDisabled.future(); 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..d1b424f314e 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); @@ -92,7 +92,7 @@ private void attemptReconnect(int retry, Handler> h if (retry > (retryConfig.getShortIntervalAttempts() + retryConfig.getLongIntervalAttempts())) { logger.info("Confiant Redis connection is not established"); } else { - long backoff = retry < retryConfig.getShortIntervalAttempts() + final long backoff = retry < retryConfig.getShortIntervalAttempts() ? retryConfig.getShortInterval() : retryConfig.getLongInterval(); 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..4dfca9b2449 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; @@ -36,7 +36,7 @@ public BidsScanResult parseBidsScanResult(String redisResponse) { } catch (JsonProcessingException resultParse) { String message; try { - RedisError errorResponse = objectMapper.readValue(redisResponse, RedisError.class); + final RedisError errorResponse = objectMapper.readValue(redisResponse, RedisError.class); message = String.format("Redis error - %s: %s", errorResponse.getCode(), errorResponse.getMessage()); } catch (JsonProcessingException errorParse) { message = String.format("Error during parse redis response: %s", redisResponse); diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/model/RedisRetryConfig.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/model/RedisRetryConfig.java index 60034a8345d..3c7f83d7164 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/model/RedisRetryConfig.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/model/RedisRetryConfig.java @@ -5,15 +5,26 @@ @Data public class RedisRetryConfig { - /** Maximum attempts with short interval value to try to reconnect to Confiant's Redis server in case any connection error happens */ + /** + * Maximum attempts with short interval value to try to reconnect to + * Confiant's Redis server in case any connection error happens + */ int shortIntervalAttempts; - /** Short time interval in milliseconds after which another one attempt to connect to Redis will be executed */ + /** + * Short time interval in milliseconds after which another one attempt to connect to Redis will be executed + */ int shortInterval; - /** Maximum attempts with long interval value to try to reconnect to Confiant's Redis server in case any connection error happens. This attempts are used when short-attempts were not successful */ + /** + * Maximum attempts with long interval value to try to reconnect to + * Confiant's Redis server in case any connection error happens. + * This attempts are used when short-attempts were not successful + */ int longIntervalAttempts; - /** Long time interval in milliseconds after which another one attempt to connect to Redis will be executed */ + /** + * Long time interval in milliseconds after which another one attempt to connect to Redis will be executed + */ int longInterval; } 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..d9a2146852e 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 @@ -4,6 +4,7 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; +import org.apache.commons.collections4.ListUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; @@ -12,13 +13,13 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.core.AnalyticsMapper; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsMapper; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -31,7 +32,6 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; public class ConfiantAdQualityBidResponsesScanHook implements AllProcessedBidResponsesHook { @@ -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() @@ -117,7 +117,7 @@ private InvocationResult toInvocationResult( .analyticsTags(AnalyticsMapper.toAnalyticsTags( bidderResponsesWithIssues, bidderResponsesWithoutIssues, notScannedBidderResponses)) .payloadUpdate(payload -> AllProcessedBidResponsesPayloadImpl.of( - Stream.concat(bidderResponsesWithoutIssues.stream(), notScannedBidderResponses.stream()).toList())); + ListUtils.union(bidderResponsesWithoutIssues, notScannedBidderResponses))); return resultBuilder.build(); } diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java deleted file mode 100644 index 76fa5759644..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.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 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/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index 4453cb34e12..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 34beae0b73b..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java deleted file mode 100644 index 439552f562f..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java deleted file mode 100644 index 1c01790d6b8..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} 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..16caae6a684 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,17 +1,18 @@ 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.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.List; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.com.confiant.adquality.core.AnalyticsMapper.toAnalyticsTags; public class AnalyticsMapperTest { @@ -29,7 +30,10 @@ public void shouldMapBidsScanResultToAnalyticsTags() { AdQualityModuleTestUtils.getBidderResponse("bidder_d", "imp_d", "bid_id_d")); // when - final Tags tags = AnalyticsMapper.toAnalyticsTags(bidderResponsesWithIssues, bidderResponsesWithoutIssues, bidderResponsesNotScanned); + final Tags tags = toAnalyticsTags( + bidderResponsesWithIssues, + bidderResponsesWithoutIssues, + bidderResponsesNotScanned); // then assertThat(tags.activities()).isEqualTo(singletonList(ActivityImpl.of( 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..3d168eaeedd 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; @@ -41,9 +41,10 @@ public void shouldMapBidResponsesToRedisBids() { assertThat(redisBidResponseData1.getBidresponse().getId()).isEqualTo(bidRequest.getId()); assertThat(redisBidResponseData1.getBidresponse().getCur()).isEqualTo(bidRequest.getCur().get(0)); assertThat(redisBidResponseData1.getBidresponse().getSeatbid()).hasSize(1); - SeatBid seatBid1 = redisBidResponseData1.getBidresponse().getSeatbid().get(0); + final SeatBid seatBid1 = redisBidResponseData1.getBidresponse().getSeatbid().get(0); assertThat(seatBid1.getBid()).hasSize(1); - assertThat(seatBid1.getBid().get(0).getId()).isEqualTo(bidderResponse1.getSeatBid().getBids().get(0).getBid().getId()); + assertThat(seatBid1.getBid().getFirst().getId()) + .isEqualTo(bidderResponse1.getSeatBid().getBids().getFirst().getBid().getId()); final RedisBidResponseData redisBidResponseData2 = result.getBresps().get(1); assertThat(redisBidResponseData2.getDspId()).isEqualTo(bidderResponse2.getBidder()); @@ -53,6 +54,7 @@ public void shouldMapBidResponsesToRedisBids() { final SeatBid seatBid2 = redisBidResponseData2.getBidresponse().getSeatbid().get(0); assertThat(seatBid2.getBid()).hasSize(1); - assertThat(seatBid2.getBid().get(0).getId()).isEqualTo(bidderResponse2.getSeatBid().getBids().get(0).getBid().getId()); + assertThat(seatBid2.getBid().getFirst().getId()) + .isEqualTo(bidderResponse2.getSeatBid().getBids().getFirst().getBid().getId()); } } 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..fd6377f9a18 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,14 +1,14 @@ 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; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils.getBidderResponse; public class BidsScanResultTest { @@ -17,7 +17,16 @@ public class BidsScanResultTest { @Test public void shouldProperlyGetIssuesMessage() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"issues\": [{ \"value\": \"ads.deceivenetworks.net\", \"spec_name\": \"malicious_domain\", \"first_adinstance\": \"e91e8da982bb8b7f80100426\"}]}]]]"; + final String redisResponse = """ + [[[{ + "tag_key": "key_a", + "imp_id": "imp_a", + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]]]"""; final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(redisResponse); // when @@ -25,7 +34,11 @@ public void shouldProperlyGetIssuesMessage() { // then assertThat(issues.size()).isEqualTo(1); - assertThat(issues.get(0)).isEqualTo("key_a: [Issue(specName=malicious_domain, value=ads.deceivenetworks.net, firstAdinstance=e91e8da982bb8b7f80100426)]"); + assertThat(issues.getFirst()).isEqualTo(""" + key_a: [\ + Issue(specName=malicious_domain, \ + value=ads.deceivenetworks.net, \ + firstAdinstance=e91e8da982bb8b7f80100426)]"""); } @Test @@ -39,16 +52,31 @@ public void shouldProperlyGetDebugMessage() { // then assertThat(messages.size()).isEqualTo(1); - assertThat(messages.get(0)).isEqualTo("Error during parse redis response: invalid redis response"); + assertThat(messages.getFirst()).isEqualTo("Error during parse redis response: invalid redis response"); } @Test public void shouldProperlyGroupBiddersByIssues() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"issues\": [{ \"value\": \"ads.deceivenetworks.net\", \"spec_name\": \"malicious_domain\", \"first_adinstance\": \"e91e8da982bb8b7f80100426\"}]}],[{\"tag_key\": \"key_b\", \"imp_id\": \"imp_b\"}]]]"; + final String redisResponse = """ + [[ + [{ + "tag_key": "key_a", + "imp_id": "imp_a", + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }], + [{ + "tag_key": "key_b", + "imp_id": "imp_b" + }] + ]]"""; final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(redisResponse); - final BidderResponse br1 = AdQualityModuleTestUtils.getBidderResponse("critio1", "1", "11"); - final BidderResponse br2 = AdQualityModuleTestUtils.getBidderResponse("critio2", "2", "12"); + final BidderResponse br1 = getBidderResponse("critio1", "1", "11"); + final BidderResponse br2 = getBidderResponse("critio2", "2", "12"); // when final GroupByIssues groupByIssues = bidsScanResult.toGroupByIssues(List.of(br1, br2)); @@ -63,10 +91,20 @@ public void shouldProperlyGroupBiddersByIssues() { @Test public void shouldProperlyGroupBiddersByIssuesWithoutIssues() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\"}],[{\"tag_key\": \"key_b\", \"imp_id\": \"imp_b\"}]]]"; + final String redisResponse = """ + [[ + [{ + "tag_key": "key_a", + "imp_id": "imp_a" + }], + [{ + "tag_key": "key_b", + "imp_id": "imp_b" + }] + ]]"""; final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(redisResponse); - final BidderResponse br1 = AdQualityModuleTestUtils.getBidderResponse("critio1", "1", "11"); - final BidderResponse br2 = AdQualityModuleTestUtils.getBidderResponse("critio2", "2", "12"); + final BidderResponse br1 = getBidderResponse("critio1", "1", "11"); + final BidderResponse br2 = getBidderResponse("critio2", "2", "12"); // when final GroupByIssues groupByIssues = bidsScanResult.toGroupByIssues(List.of(br1, br2)); 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..eaedc93e170 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,17 +9,15 @@ 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; import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisBidsData; -import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; import java.util.List; @@ -27,12 +25,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; +import static org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils.getBidderResponse; +@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()); } @@ -121,9 +118,10 @@ public void shouldReturnEmptyScanResultWhenApiIsNotInitialized() { public void shouldReturnEmptyScanResultWhenThereIsNoBidderResponses() { // given doReturn(redisAPI).when(readRedisNode).getRedisAPI(); + final RedisBidsData redisBidsData = RedisBidsData.builder().bresps(List.of()).build(); // when - final Future scanResult = bidsScannerTest.submitBids(RedisBidsData.builder().bresps(List.of()).build()); + final Future scanResult = bidsScannerTest.submitBids(redisBidsData); final GroupByIssues groupByIssues = scanResult.result().toGroupByIssues(List.of()); // then @@ -135,7 +133,16 @@ public void shouldReturnEmptyScanResultWhenThereIsNoBidderResponses() { @Test() public void shouldReturnEmptyScanResultWhenThereIsSomeBidderResponseAndScanIsDisabled() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"issues\": [{ \"value\": \"ads.deceivenetworks.net\", \"spec_name\": \"malicious_domain\", \"first_adinstance\": \"e91e8da982bb8b7f80100426\"}]}]]]"; + final String redisResponse = """ + [[[{ + "tag_key": "key_a", + "imp_id": "imp_a", + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]]]"""; final RedisAPI redisAPI = getRedisEmulationWithAnswer(redisResponse); final RedisBidsData bidsData = RedisBidsData.builder() .breq(BidRequest.builder().build()) @@ -148,7 +155,7 @@ public void shouldReturnEmptyScanResultWhenThereIsSomeBidderResponseAndScanIsDis // when final Future scanResult = bidsScannerTest.submitBids(bidsData); final GroupByIssues groupByIssues = scanResult.result() - .toGroupByIssues(List.of(AdQualityModuleTestUtils.getBidderResponse("bidder-a", "imp-a", "imp-id-a"))); + .toGroupByIssues(List.of(getBidderResponse("bidder-a", "imp-a", "imp-id-a"))); // then assertThat(scanResult.succeeded()).isTrue(); @@ -159,7 +166,22 @@ public void shouldReturnEmptyScanResultWhenThereIsSomeBidderResponseAndScanIsDis @Test() public void shouldReturnRedisScanResultFromReadNodeWhenThereAreSomeBidderResponsesAndScanIsEnabled() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"issues\": [{ \"value\": \"ads.deceivenetworks.net\", \"spec_name\": \"malicious_domain\", \"first_adinstance\": \"e91e8da982bb8b7f80100426\"}]}],[{\"tag_key\": \"key_b\", \"imp_id\": \"imp_b\"}]]]"; + final String redisResponse = """ + [[ + [{ + "tag_key": "key_a", + "imp_id": "imp_a", + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }], + [{ + "tag_key": "key_b", + "imp_id": "imp_b" + }] + ]]"""; final RedisAPI redisAPI = getRedisEmulationWithAnswer(redisResponse); final RedisBidsData bidsData = RedisBidsData.builder() .breq(BidRequest.builder().build()) @@ -174,8 +196,8 @@ public void shouldReturnRedisScanResultFromReadNodeWhenThereAreSomeBidderRespons final Future scanResult = bidsScannerTest.submitBids(bidsData); final GroupByIssues groupByIssues = scanResult.result() .toGroupByIssues(List.of( - AdQualityModuleTestUtils.getBidderResponse("bidder-a", "imp-a", "imp-id-a"), - AdQualityModuleTestUtils.getBidderResponse("bidder-b", "imp-b", "imp-id-b"))); + getBidderResponse("bidder-a", "imp-a", "imp-id-a"), + getBidderResponse("bidder-b", "imp-b", "imp-id-b"))); // then assertThat(scanResult.succeeded()).isTrue(); @@ -186,7 +208,12 @@ public void shouldReturnRedisScanResultFromReadNodeWhenThereAreSomeBidderRespons @Test() public void shouldReturnRedisScanResultFromWriteNodeWhenReadNodeHasMissingResults() { // given - final String readRedisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"ro_skipped\": \"true\"}]]]"; + final String readRedisResponse = """ + [[[{ + "tag_key": "key_a", + "imp_id": "imp_a", + "ro_skipped": "true" + }]]]"""; final RedisAPI readRedisAPI = getRedisEmulationWithAnswer(readRedisResponse); final RedisBidsData bidsData = RedisBidsData.builder() .breq(BidRequest.builder().build()) @@ -197,14 +224,23 @@ public void shouldReturnRedisScanResultFromWriteNodeWhenReadNodeHasMissingResult bidsScannerTest.enableScan(); doReturn(readRedisAPI).when(readRedisNode).getRedisAPI(); - final String writeRedisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\", \"issues\": [{ \"value\": \"ads.deceivenetworks.net\", \"spec_name\": \"malicious_domain\", \"first_adinstance\": \"e91e8da982bb8b7f80100426\"}]}]]]"; + final String writeRedisResponse = """ + [[[{ + "tag_key": "key_a", + "imp_id": "imp_a", + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]]]"""; final RedisAPI writeRedisAPI = getRedisEmulationWithAnswer(writeRedisResponse); doReturn(writeRedisAPI).when(writeRedisNode).getRedisAPI(); // when final Future scanResult = bidsScannerTest.submitBids(bidsData); final GroupByIssues groupByIssues = scanResult.result() - .toGroupByIssues(List.of(AdQualityModuleTestUtils.getBidderResponse("bidder-a", "imp-a", "imp-id-a"))); + .toGroupByIssues(List.of(getBidderResponse("bidder-a", "imp-a", "imp-id-a"))); // then assertThat(scanResult.succeeded()).isTrue(); 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..28d889c48b6 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; @@ -12,7 +12,17 @@ public class RedisParserTest { @Test public void shouldParseBidsScanResult() { // given - final String redisResponse = "[[[{\"tag_key\": \"key_a\", \"imp_id\": \"imp_a\"}]],[[{\"tag_key\": \"key_b\", \"imp_id\": \"imp_b\"}]]]"; + final String redisResponse = """ + [ + [[{ + "tag_key": "key_a", + "imp_id": "imp_a" + }]], + [[{ + "tag_key": "key_b", + "imp_id": "imp_b" + }]] + ]"""; // when final BidsScanResult actualScanResults = redisParser.parseBidsScanResult(redisResponse); @@ -28,58 +38,59 @@ public void shouldParseBidsScanResult() { @Test public void shouldParseFullBidsScanResult() { // given - final String redisResponse = "[[[{\n" + - " \"tag_key\": \"tg\",\n" + - " \"imp_id\": \"123\",\n" + - " \"known_creative\": true,\n" + - " \"ro_skipped\": false,\n" + - " \"issues\": [{\n" + - " \"value\": \"ads.deceivenetworks.net\",\n" + - " \"spec_name\": \"malicious_domain\",\n" + - " \"first_adinstance\": \"e91e8da982bb8b7f80100426\"\n" + - " }],\n" + - " \"attributes\": {\n" + - " \"is_ssl\": true,\n" + - " \"ssl_error\": false,\n" + - " \"width\": 600,\n" + - " \"height\": 300,\n" + - " \"anim\": 5,\n" + - " \"network_load_startup\": 1024,\n" + - " \"network_load_polite\": 1024,\n" + - " \"vast\": {\n" + - " \"redirects\": 3\n" + - " },\n" + - " \"brands\": [\n" + - " \"Pfizer\"\n" + - " ],\n" + - " \"categories\": [\n" + - " {\n" + - " \"code\": \"CAT-2\",\n" + - " \"name\": \"Health and Medical Services\"\n" + - " },\n" + - " {\n" + - " \"code\": \"CAT-75\",\n" + - " \"name\": \"Pharmaceutical Drugs\"\n" + - " }\n" + - " ]\n" + - " },\n" + - " \"metrics\": {\n" + - " \"submitted\": \"2017-05-10T13:29:28-04:00\",\n" + - " \"fetched\":\"2017-05-10T13:29:29-04:00\",\n" + - " \"scanned\":\"2017-07-22T11:49:40-04:00\",\n" + - " \"synchronized\": {\n" + - " \"first\":\"2017-05-10T13:29:55-04:00\",\n" + - " \"last\":\"2017-07-24T00:52:04-04:00\"\n" + - " }\n" + - " },\n" + - " \"adinstance\": \"qwerty\"\n" + - "}]]]"; + final String redisResponse = """ + [[[{ + "tag_key": "tg", + "imp_id": "123", + "known_creative": true, + "ro_skipped": false, + "issues": [{ + "value": "ads.deceivenetworks.net", + "spec_name": "malicious_domain", + "first_adinstance": "e91e8da982bb8b7f80100426" + }], + "attributes": { + "is_ssl": true, + "ssl_error": false, + "width": 600, + "height": 300, + "anim": 5, + "network_load_startup": 1024, + "network_load_polite": 1024, + "vast": { + "redirects": 3 + }, + "brands": [ + "Pfizer" + ], + "categories": [ + { + "code": "CAT-2", + "name": "Health and Medical Services" + }, + { + "code": "CAT-75", + "name": "Pharmaceutical Drugs" + } + ] + }, + "metrics": { + "submitted": "2017-05-10T13:29:28-04:00", + "fetched":"2017-05-10T13:29:29-04:00", + "scanned":"2017-07-22T11:49:40-04:00", + "synchronized": { + "first":"2017-05-10T13:29:55-04:00", + "last":"2017-07-24T00:52:04-04:00" + } + }, + "adinstance": "qwerty" + }]]]"""; // when final BidsScanResult actualScanResults = redisParser.parseBidsScanResult(redisResponse); // then - assertThat(actualScanResults.getBidScanResults().get(0).getTagKey()).isEqualTo("tg"); + assertThat(actualScanResults.getBidScanResults().getFirst().getTagKey()).isEqualTo("tg"); assertThat(actualScanResults.getBidScanResults().size()).isEqualTo(1); assertThat(actualScanResults.getDebugMessages().size()).isEqualTo(0); } @@ -87,14 +98,15 @@ public void shouldParseFullBidsScanResult() { @Test public void shouldParseBidsScanResultWithError() { // given - final String redisResponse = "{\"code\": \"123\", \"message\": \"error message\", \"error\": true, \"dsp_id\": \"cri\"}"; + final String redisResponse = """ + {"code": "123", "message": "error message", "error": true, "dsp_id": "cri"}"""; // when final BidsScanResult actualScanResults = redisParser.parseBidsScanResult(redisResponse); // then assertThat(actualScanResults.getBidScanResults().size()).isEqualTo(0); - assertThat(actualScanResults.getDebugMessages().get(0)).isEqualTo("Redis error - 123: error message"); + assertThat(actualScanResults.getDebugMessages().getFirst()).isEqualTo("Redis error - 123: error message"); } @Test @@ -107,6 +119,7 @@ public void shouldParseBidsScanResultWithInvalidResponse() { // then assertThat(actualScanResults.getBidScanResults().size()).isEqualTo(0); - assertThat(actualScanResults.getDebugMessages().get(0)).isEqualTo("Error during parse redis response: invalid redis response"); + assertThat(actualScanResults.getDebugMessages().getFirst()) + .isEqualTo("Error during parse redis response: invalid redis response"); } } 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/util/AdQualityModuleTestUtils.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/util/AdQualityModuleTestUtils.java index 865214fd648..594bbb08445 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/util/AdQualityModuleTestUtils.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/util/AdQualityModuleTestUtils.java @@ -12,18 +12,24 @@ public class AdQualityModuleTestUtils { + private AdQualityModuleTestUtils() { + } + public static BidderResponse getBidderResponse(String bidderName, String impId, String bidId) { - return BidderResponse.of(bidderName, BidderSeatBid.builder() - .bids(Collections.singletonList(BidderBid.builder() - .type(BidType.banner) - .bid(Bid.builder() - .id(bidId) - .price(BigDecimal.valueOf(11)) - .impid(impId) - .adm("adm") - .adomain(List.of("www.goog.com", "www.gumgum.com")) - .build()) - .build())) - .build(), 11); + return BidderResponse.of( + bidderName, + BidderSeatBid.builder() + .bids(Collections.singletonList(BidderBid.builder() + .type(BidType.banner) + .bid(Bid.builder() + .id(bidId) + .price(BigDecimal.valueOf(11)) + .impid(impId) + .adm("adm") + .adomain(List.of("www.goog.com", "www.gumgum.com")) + .build()) + .build())) + .build(), + 11); } } 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..47ca8d9b86f 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,26 +6,24 @@ 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; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsMapper; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.core.RedisParser; -import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -42,12 +40,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; +import static org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils.getBidderResponse; +@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"); } @@ -112,12 +105,19 @@ public void callShouldReturnResultWithNoActionWhenRedisHasNoAnswer() { @Test public void callShouldReturnResultWithUpdateActionWhenRedisHasFoundSomeIssues() { // given - final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult( - "[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]]]"); + final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(""" + [[[{ + "tag_key": "tag", + "issues": [{ + "spec_name": "malicious_domain", + "value": "ads.deceivenetworks.net", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]]]"""); doReturn(Future.succeededFuture(bidsScanResult)).when(bidsScanner).submitBids(any()); doReturn(getAuctionContext()).when(auctionInvocationContext).auctionContext(); - doReturn(List.of(AdQualityModuleTestUtils.getBidderResponse("bidder_a", "imp_a", "bid_id_a"))) + doReturn(List.of(getBidderResponse("bidder_a", "imp_a", "bid_id_a"))) .when(allProcessedBidResponsesPayload).bidResponses(); // when @@ -132,7 +132,12 @@ public void callShouldReturnResultWithUpdateActionWhenRedisHasFoundSomeIssues() assertThat(result).isNotNull(); assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors().get(0)).isEqualTo("tag: [Issue(specName=malicious_domain, value=ads.deceivenetworks.net, firstAdinstance=e91e8da982bb8b7f80100426)]"); + assertThat(result.errors().getFirst()) + .isEqualTo(""" + tag: [\ + Issue(specName=malicious_domain, \ + value=ads.deceivenetworks.net, \ + firstAdinstance=e91e8da982bb8b7f80100426)]"""); assertThat(result.debugMessages()).isNull(); assertThat(result.analyticsTags().activities()).isEqualTo(singletonList(ActivityImpl.of( "ad-scan", "success", List.of( @@ -147,8 +152,15 @@ public void callShouldReturnResultWithUpdateActionWhenRedisHasFoundSomeIssues() @Test public void callShouldSubmitBidsToScanWhenBidsCome() { // given - final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult( - "[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]]]"); + final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(""" + [[[{ + "tag_key": "tag", + "issues": [{ + "spec_name": "malicious_domain", + "value": "ads.deceivenetworks.net", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]]]"""); doReturn(Future.succeededFuture(bidsScanResult)).when(bidsScanner).submitBids(any()); doReturn(getAuctionContext()).when(auctionInvocationContext).auctionContext(); @@ -161,16 +173,32 @@ public void callShouldSubmitBidsToScanWhenBidsCome() { } @Test - public void callShouldSubmitToScanBidsWhichAreNotPartOfTheExcludeToScanListWhenHookIsConfiguredWithExcludeToScanList() { + public void callShouldSubmitBidsWhichAreNotPartOfTheExcludeToScanListWhenHookIsConfiguredWithExcludeToScanList() { // given final String secureBidderName = "securebidder"; final String notSecureBadBidderName = "notsecurebadbidder"; final String notSecureGoodBidderName = "notsecuregoodbidder"; - final BidderResponse secureBidderResponse = AdQualityModuleTestUtils.getBidderResponse(secureBidderName, "imp_a", "bid_id_a"); - final BidderResponse notSecureBadBidderResponse = AdQualityModuleTestUtils.getBidderResponse(notSecureBadBidderName, "imp_b", "bid_id_b"); - final BidderResponse notSecureGoodBidderResponse = AdQualityModuleTestUtils.getBidderResponse(notSecureGoodBidderName, "imp_c", "bid_id_c"); - final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult( - "[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]],[[{\"tag_key\": \"key_b\", \"imp_id\": \"imp_b\", \"issues\": []}]]]]"); + final BidderResponse secureBidderResponse = getBidderResponse(secureBidderName, "imp_a", "bid_id_a"); + final BidderResponse notSecureBadBidderResponse = + getBidderResponse(notSecureBadBidderName, "imp_b", "bid_id_b"); + final BidderResponse notSecureGoodBidderResponse = + getBidderResponse(notSecureGoodBidderName, "imp_c", "bid_id_c"); + final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(""" + [ + [[{ + "tag_key": "tag", + "issues": [{ + "spec_name": "malicious_domain", + "value": "ads.deceivenetworks.net", + "first_adinstance": "e91e8da982bb8b7f80100426" + }] + }]], + [[{ + "tag_key": "key_b", + "imp_id": "imp_b", + "issues": [] + }]] + ]"""); final AuctionContext auctionContext = AuctionContext.builder() .activityInfrastructure(activityInfrastructure) .bidRequest(BidRequest.builder().cur(List.of("USD")).build()) @@ -178,7 +206,8 @@ public void callShouldSubmitToScanBidsWhichAreNotPartOfTheExcludeToScanListWhenH target = new ConfiantAdQualityBidResponsesScanHook(bidsScanner, List.of(secureBidderName), userFpdActivityMask); - doReturn(List.of(secureBidderResponse, notSecureBadBidderResponse, notSecureGoodBidderResponse)).when(allProcessedBidResponsesPayload).bidResponses(); + doReturn(List.of(secureBidderResponse, notSecureBadBidderResponse, notSecureGoodBidderResponse)) + .when(allProcessedBidResponsesPayload).bidResponses(); doReturn(Future.succeededFuture(bidsScanResult)).when(bidsScanner).submitBids(any()); doReturn(auctionContext).when(auctionInvocationContext).auctionContext(); @@ -187,9 +216,9 @@ public void callShouldSubmitToScanBidsWhichAreNotPartOfTheExcludeToScanListWhenH .call(allProcessedBidResponsesPayload, auctionInvocationContext); // then - verify(bidsScanner).submitBids( - BidsMapper.toRedisBidsFromBidResponses(auctionContext.getBidRequest(), List.of(notSecureBadBidderResponse, notSecureGoodBidderResponse)) - ); + verify(bidsScanner).submitBids(BidsMapper.toRedisBidsFromBidResponses( + auctionContext.getBidRequest(), + List.of(notSecureBadBidderResponse, notSecureGoodBidderResponse))); final PayloadUpdate payloadUpdate = invocationResult.result().payloadUpdate(); final AllProcessedBidResponsesPayloadImpl initPayloadToUpdate = AllProcessedBidResponsesPayloadImpl.of( @@ -224,11 +253,19 @@ public void callShouldSubmitToScanOnlyBidsWithDataWhenSomeBiddersRespondWithEmpt final String secureBidderName = "securebidder"; final String notSecureBadBidderName = "notsecurebadbidder"; final String emptyBidderName = "emptybidder"; - final BidderResponse secureBidderResponse = AdQualityModuleTestUtils.getBidderResponse(secureBidderName, "imp_a", "bid_id_a"); - final BidderResponse notSecureBadBidderResponse = AdQualityModuleTestUtils.getBidderResponse(notSecureBadBidderName, "imp_b", "bid_id_b"); - final BidderResponse emptyBidderResponse = getEmptyBidderResponse(emptyBidderName); - final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult( - "[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]]]"); + final BidderResponse secureBidderResponse = getBidderResponse(secureBidderName, "imp_a", "bid_id_a"); + final BidderResponse notSecureBadBidderResponse = + getBidderResponse(notSecureBadBidderName, "imp_b", "bid_id_b"); + final BidderResponse emptyBidderResponse = getEmptyBidderResponse(); + final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(""" + [[[{ + "tag_key": "tag", + "issues": [{ + "spec_name": "malicious_domain", + "value": "ads.deceivenetworks.net", + "first_adinstance":"e91e8da982bb8b7f80100426" + }] + }]]]"""); final AuctionContext auctionContext = AuctionContext.builder() .activityInfrastructure(activityInfrastructure) .bidRequest(BidRequest.builder().cur(List.of("USD")).build()) @@ -236,7 +273,8 @@ public void callShouldSubmitToScanOnlyBidsWithDataWhenSomeBiddersRespondWithEmpt target = new ConfiantAdQualityBidResponsesScanHook(bidsScanner, List.of(secureBidderName), userFpdActivityMask); - doReturn(List.of(secureBidderResponse, notSecureBadBidderResponse, emptyBidderResponse)).when(allProcessedBidResponsesPayload).bidResponses(); + doReturn(List.of(secureBidderResponse, notSecureBadBidderResponse, emptyBidderResponse)) + .when(allProcessedBidResponsesPayload).bidResponses(); doReturn(Future.succeededFuture(bidsScanResult)).when(bidsScanner).submitBids(any()); doReturn(auctionContext).when(auctionInvocationContext).auctionContext(); @@ -245,9 +283,8 @@ public void callShouldSubmitToScanOnlyBidsWithDataWhenSomeBiddersRespondWithEmpt .call(allProcessedBidResponsesPayload, auctionInvocationContext); // then - verify(bidsScanner).submitBids( - BidsMapper.toRedisBidsFromBidResponses(auctionContext.getBidRequest(), List.of(notSecureBadBidderResponse)) - ); + verify(bidsScanner).submitBids(BidsMapper.toRedisBidsFromBidResponses( + auctionContext.getBidRequest(), List.of(notSecureBadBidderResponse))); final PayloadUpdate payloadUpdate = invocationResult.result().payloadUpdate(); final AllProcessedBidResponsesPayloadImpl initPayloadToUpdate = AllProcessedBidResponsesPayloadImpl.of( @@ -267,18 +304,23 @@ public void callShouldSubmitToScanOnlyBidsWithDataWhenSomeBiddersRespondWithEmpt .bidders(List.of(notSecureBadBidderName)) .impIds(List.of("imp_b")) .bidIds(List.of("bid_id_b")) - .build())) - ))); + .build()))))); } @Test public void callShouldSubmitBidsWithoutMaskedGeoInfoWhenTransmitGeoIsAllowed() { // given 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 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); final Device device = userFpdActivityMask.maskDevice( getDevice(), true, !transmitGeoIsAllowed); @@ -304,10 +346,16 @@ public void callShouldSubmitBidsWithoutMaskedGeoInfoWhenTransmitGeoIsAllowed() { public void callShouldSubmitBidsWithMaskedGeoInfoWhenTransmitGeoIsNotAllowed() { // given 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 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); final Device device = userFpdActivityMask.maskDevice( getDevice(), true, !transmitGeoIsAllowed); @@ -351,7 +399,7 @@ public void callShouldReturnResultWithDebugInfoWhenDebugIsEnabledAndRequestIsBro assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.action()).isEqualTo(InvocationAction.no_action); assertThat(result.errors()).isNull(); - assertThat(result.debugMessages().get(0)).isEqualTo("Error during parse redis response: [[[{\"t"); + assertThat(result.debugMessages().getFirst()).isEqualTo("Error during parse redis response: [[[{\"t"); } @Test @@ -398,8 +446,8 @@ private static Device getDevice() { return Device.builder().geo(Geo.builder().country("country-d").region("region-d").build()).build(); } - private static BidderResponse getEmptyBidderResponse(String bidderName) { - return BidderResponse.of(bidderName, BidderSeatBid.builder() + private static BidderResponse getEmptyBidderResponse() { + return BidderResponse.of("emptybidder", BidderSeatBid.builder() .bids(Collections.emptyList()) .build(), 5); } 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..aafbfa859ac --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + fiftyone-devicedetection + + fiftyone-devicedetection + 51Degrees Device Detection module + + + 4.4.226 + + + + + + com.51degrees + device-detection.hash.engine.on-premise + ${fiftyone-device-detection.version} + + + + + com.51degrees + device-detection + ${fiftyone-device-detection.version} + + + 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..ee93c1e3a76 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/config/FiftyOneDeviceDetectionModuleConfiguration.java @@ -0,0 +1,50 @@ +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.model.config.ModuleConfig; +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.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..ac427e9d9e7 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/boundary/CollectedEvidence.java @@ -0,0 +1,13 @@ +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..c7cda11450a --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilter.java @@ -0,0 +1,11 @@ +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..46cf19adf56 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFile.java @@ -0,0 +1,13 @@ +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..8c65b7d4508 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdate.java @@ -0,0 +1,19 @@ +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..80f95f353e5 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfig.java @@ -0,0 +1,13 @@ +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..7a81b11be5a --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfig.java @@ -0,0 +1,17 @@ +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..72b1e04cee2 --- /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..5b0e048f5b9 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/EnrichmentResult.java @@ -0,0 +1,12 @@ +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..fc5a6c8d9ed --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java @@ -0,0 +1,40 @@ +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.PHONE), + 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..99bb8de408c --- /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..cc47233e68a --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetriever.java @@ -0,0 +1,101 @@ +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 jakarta.annotation.Nonnull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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..44788286dd3 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java @@ -0,0 +1,43 @@ +package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks; + +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +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.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; + +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..a0d91e8bb0a --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java @@ -0,0 +1,154 @@ +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 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.hooks.execution.v1.InvocationResultImpl; +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.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 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/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..424d08db123 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilterTest.java @@ -0,0 +1,32 @@ +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..d56531ef68a --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileTest.java @@ -0,0 +1,58 @@ +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..fa3790e3261 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdateTest.java @@ -0,0 +1,96 @@ +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..3157c9167d2 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfigTest.java @@ -0,0 +1,66 @@ +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..829f5298fa1 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfigTest.java @@ -0,0 +1,83 @@ +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..e3ca77b5e24 --- /dev/null +++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModuleTest.java @@ -0,0 +1,33 @@ +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..0aa2610f62c --- /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/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml new file mode 100644 index 00000000000..141e4cf087e --- /dev/null +++ b/extra/modules/greenbids-real-time-data/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + greenbids-real-time-data + + greenbids-real-time-data + Greenbids Real Time Data + + + 1.6.1 + 1.21.0 + 2.50.0 + + + + + com.github.ua-parser + uap-java + ${uap-java.version} + + + + com.microsoft.onnxruntime + onnxruntime + ${onnxruntime.version} + + + + com.google.cloud + google-cloud-storage + ${google-cloud-storage.version} + + + commons-logging + commons-logging + + + com.google.guava + failureaccess + + + + + + diff --git a/extra/modules/greenbids-real-time-data/src/lombok.config b/extra/modules/greenbids-real-time-data/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java new file mode 100644 index 00000000000..19155c86e8f --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java @@ -0,0 +1,131 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import com.maxmind.db.Reader; +import com.maxmind.geoip2.DatabaseReader; +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.FileSystem; +import io.vertx.core.file.OpenOptions; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.vertx.Initializable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPInputStream; + +public class DatabaseReaderFactory implements Initializable { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseReaderFactory.class); + + private final GreenbidsRealTimeDataProperties properties; + + private final Vertx vertx; + + private final AtomicReference databaseReaderRef = new AtomicReference<>(); + + private final FileSystem fileSystem; + + public DatabaseReaderFactory(GreenbidsRealTimeDataProperties properties, Vertx vertx) { + this.properties = properties; + this.vertx = vertx; + this.fileSystem = vertx.fileSystem(); + } + + @Override + public void initialize(Promise initializePromise) { + downloadAndExtract() + .onSuccess(databaseReaderRef::set) + .mapEmpty() + .onComplete(initializePromise); + } + + private Future downloadAndExtract() { + final String downloadUrl = properties.getGeoLiteCountryPath(); + final String tmpPath = properties.getTmpPath(); + return downloadFile(downloadUrl, tmpPath) + .compose(ignored -> vertx.executeBlocking(() -> extractMMDB(tmpPath))) + .onComplete(ar -> removeFile(tmpPath)); + } + + private Future downloadFile(String downloadUrl, String tmpPath) { + return fileSystem.open(tmpPath, new OpenOptions()) + .compose(tmpFile -> sendHttpRequest(downloadUrl) + .onFailure(ignore -> tmpFile.close()) + .compose(response -> response.pipeTo(tmpFile))); + } + + private Future sendHttpRequest(String url) { + final RequestOptions options = new RequestOptions() + .setFollowRedirects(true) + .setMethod(HttpMethod.GET) + .setTimeout(properties.getTimeoutMs()) + .setAbsoluteURI(url); + + final HttpClientOptions httpClientOptions = new HttpClientOptions() + .setConnectTimeout(properties.getTimeoutMs().intValue()) + .setMaxRedirects(properties.getMaxRedirects()); + + return vertx.createHttpClient(httpClientOptions).request(options) + .compose(HttpClientRequest::send) + .map(this::validateResponse); + } + + private HttpClientResponse validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException("Got unexpected response from server with status code %s and message %s" + .formatted(statusCode, response.statusMessage())); + } + return response; + } + + private DatabaseReader extractMMDB(String tarGzPath) { + try (GZIPInputStream gis = new GZIPInputStream(Files.newInputStream(Path.of(tarGzPath))); + TarArchiveInputStream tarInput = new TarArchiveInputStream(gis)) { + + TarArchiveEntry currentEntry; + boolean hasDatabaseFile = false; + while ((currentEntry = tarInput.getNextEntry()) != null) { + if (currentEntry.getName().contains("GeoLite2-Country.mmdb")) { + hasDatabaseFile = true; + break; + } + } + + if (!hasDatabaseFile) { + throw new PreBidException("GeoLite2-Country.mmdb not found in the archive"); + } + + return new DatabaseReader.Builder(tarInput) + .fileMode(Reader.FileMode.MEMORY).build(); + } catch (IOException e) { + throw new PreBidException("Failed to extract MMDB file", e); + } + } + + private void removeFile(String filePath) { + fileSystem.exists(filePath).onSuccess(exists -> { + if (exists) { + fileSystem.delete(filePath) + .onFailure(err -> logger.error("Failed to remove file {}", filePath, err)); + } + }); + } + + public DatabaseReader getDatabaseReader() { + return databaseReaderRef.get(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java new file mode 100644 index 00000000000..ec8cb4c10b1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import io.vertx.core.Vertx; +import org.prebid.server.geolocation.CountryCodeMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.GreenbidsRealTimeDataProcessedAuctionRequestHook; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@ConditionalOnProperty(prefix = "hooks." + GreenbidsRealTimeDataModule.CODE, name = "enabled", havingValue = "true") +@Configuration +@EnableConfigurationProperties(GreenbidsRealTimeDataProperties.class) +public class GreenbidsRealTimeDataConfiguration { + + @Bean + DatabaseReaderFactory databaseReaderFactory(GreenbidsRealTimeDataProperties properties, Vertx vertx) { + return new DatabaseReaderFactory(properties, vertx); + } + + @Bean + GreenbidsInferenceDataService greenbidsInferenceDataService(DatabaseReaderFactory databaseReaderFactory, + CountryCodeMapper countryCodeMapper) { + + return new GreenbidsInferenceDataService( + databaseReaderFactory, ObjectMapperProvider.mapper(), countryCodeMapper); + } + + @Bean + GreenbidsRealTimeDataModule greenbidsRealTimeDataModule( + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService) { + + return new GreenbidsRealTimeDataModule(List.of( + new GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapperProvider.mapper(), + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService))); + } + + @Bean + FilterService filterService() { + return new FilterService(); + } + + @Bean + Storage storage(GreenbidsRealTimeDataProperties properties) { + return StorageOptions.newBuilder() + .setProjectId(properties.getGoogleCloudGreenbidsProject()).build().getService(); + } + + @Bean + OnnxModelRunnerFactory onnxModelRunnerFactory() { + return new OnnxModelRunnerFactory(); + } + + @Bean + ThrottlingThresholdsFactory throttlingThresholdsFactory() { + return new ThrottlingThresholdsFactory(); + } + + @Bean + ModelCache modelCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + + final Cache modelCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ModelCache( + storage, + properties.getGcsBucketName(), + modelCacheWithExpiration, + properties.getOnnxModelCacheKeyPrefix(), + vertx, + onnxModelRunnerFactory); + } + + @Bean + ThresholdCache thresholdCache( + GreenbidsRealTimeDataProperties properties, + Vertx vertx, + Storage storage, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + + final Cache thresholdsCacheWithExpiration = Caffeine.newBuilder() + .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES) + .build(); + + return new ThresholdCache( + storage, + properties.getGcsBucketName(), + ObjectMapperProvider.mapper(), + thresholdsCacheWithExpiration, + properties.getThresholdsCacheKeyPrefix(), + vertx, + throttlingThresholdsFactory); + } + + @Bean + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds(ModelCache modelCache, ThresholdCache thresholdCache) { + return new OnnxModelRunnerWithThresholds(modelCache, thresholdCache); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java new file mode 100644 index 00000000000..b2e5bdcfeb8 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +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.List; + +public class GreenbidsRealTimeDataModule implements Module { + + public static final String CODE = "greenbids-real-time-data"; + + private final List> hooks; + + public GreenbidsRealTimeDataModule(List> hooks) { + this.hooks = hooks; + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java new file mode 100644 index 00000000000..4752a2e8840 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "hooks.modules." + GreenbidsRealTimeDataModule.CODE) +@Data +public class GreenbidsRealTimeDataProperties { + + String googleCloudGreenbidsProject; + + String geoLiteCountryPath; + + String tmpPath; + + String gcsBucketName; + + Integer cacheExpirationMinutes; + + String onnxModelCacheKeyPrefix; + + String thresholdsCacheKeyPrefix; + + Long timeoutMs; + + Integer maxRedirects; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java new file mode 100644 index 00000000000..094c2d18df1 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java @@ -0,0 +1,123 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class FilterService { + + public Map> filterBidders( + OnnxModelRunner onnxModelRunner, + List throttlingMessages, + Double threshold) { + + final OrtSession.Result results; + try { + final String[][] throttlingInferenceRows = convertToArray(throttlingMessages); + results = onnxModelRunner.runModel(throttlingInferenceRows); + return processModelResults(results, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception during model inference: ", e); + } + } + + private static String[][] convertToArray(List messages) { + return messages.stream() + .map(message -> new String[]{ + message.getBrowser(), + message.getBidder(), + message.getAdUnitCode(), + message.getCountry(), + message.getHostname(), + message.getDevice(), + message.getHourBucket(), + message.getMinuteQuadrant()}) + .toArray(String[][]::new); + } + + private Map> processModelResults( + OrtSession.Result results, + List throttlingMessages, + Double threshold) { + + validateThrottlingMessages(throttlingMessages); + + return StreamSupport.stream(results.spliterator(), false) + .peek(FilterService::validateOnnxTensor) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .peek(tensor -> validateTensorSize(tensor, throttlingMessages.size())) + .map(tensor -> extractAndProcessProbabilities(tensor, throttlingMessages, threshold)) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static void validateThrottlingMessages(List throttlingMessages) { + if (throttlingMessages == null || CollectionUtils.isEmpty(throttlingMessages)) { + throw new PreBidException("throttlingMessages cannot be null or empty"); + } + } + + private static void validateOnnxTensor(Map.Entry onnxItem) { + if (!(onnxItem.getValue() instanceof OnnxTensor)) { + throw new PreBidException("Expected OnnxTensor for 'probabilities', but found: " + + onnxItem.getValue().getClass().getName()); + } + } + + private static void validateTensorSize(OnnxTensor tensor, int expectedSize) { + final long[] tensorShape = tensor.getInfo().getShape(); + if (tensorShape.length == 0 || tensorShape[0] != expectedSize) { + throw new PreBidException("Mismatch between tensor size and throttlingMessages size"); + } + } + + private Map> extractAndProcessProbabilities( + OnnxTensor tensor, + List throttlingMessages, + Double threshold) { + + try { + final float[][] probabilities = extractProbabilitiesValues(tensor); + return processProbabilities(probabilities, throttlingMessages, threshold); + } catch (OrtException e) { + throw new PreBidException("Exception when extracting proba from OnnxTensor: ", e); + } + } + + private float[][] extractProbabilitiesValues(OnnxTensor tensor) throws OrtException { + return (float[][]) tensor.getValue(); + } + + private Map> processProbabilities( + float[][] probabilities, + List throttlingMessages, + Double threshold) { + + final Map> result = new HashMap<>(); + + for (int i = 0; i < probabilities.length; i++) { + final ThrottlingMessage message = throttlingMessages.get(i); + final String impId = message.getAdUnitCode(); + final String bidder = message.getBidder(); + final boolean isKeptInAuction = probabilities[i][1] > threshold; + result.computeIfAbsent(impId, k -> new HashMap<>()).put(bidder, isKeptInAuction); + } + + return result; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java new file mode 100644 index 00000000000..ce0b9f3f806 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java @@ -0,0 +1,207 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.geolocation.CountryCodeMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class GreenbidsInferenceDataService { + + private final DatabaseReaderFactory databaseReaderFactory; + + private final ObjectMapper mapper; + + private final CountryCodeMapper countryCodeMapper; + + public GreenbidsInferenceDataService(DatabaseReaderFactory dbReaderFactory, + ObjectMapper mapper, + CountryCodeMapper countryCodeMapper) { + this.databaseReaderFactory = Objects.requireNonNull(dbReaderFactory); + this.mapper = Objects.requireNonNull(mapper); + this.countryCodeMapper = Objects.requireNonNull(countryCodeMapper); + } + + public List extractThrottlingMessagesFromBidRequest(BidRequest bidRequest) { + final GreenbidsUserAgent userAgent = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getUa) + .map(GreenbidsUserAgent::new) + .orElse(null); + + return extractThrottlingMessages(bidRequest, userAgent); + } + + private List extractThrottlingMessages( + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent) { + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer hourBucket = timestamp.getHour(); + final Integer minuteQuadrant = (timestamp.getMinute() / 15) + 1; + + final String hostname = bidRequest.getSite().getDomain(); + final List imps = bidRequest.getImp(); + + return imps.stream() + .map(imp -> extractMessagesForImp( + imp, + bidRequest, + greenbidsUserAgent, + hostname, + hourBucket, + minuteQuadrant)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List extractMessagesForImp( + Imp imp, + BidRequest bidRequest, + GreenbidsUserAgent greenbidsUserAgent, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String impId = imp.getId(); + final ObjectNode impExt = imp.getExt(); + final JsonNode bidderNode = extImpPrebid(impExt.get("prebid")).getBidder(); + final String ip = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getIp) + .orElse(null); + final String country = Optional.ofNullable(bidRequest.getDevice()) + .map(Device::getGeo) + .map(Geo::getCountry) + .map(countryCodeMapper::mapToAlpha2) + .map(GreenbidsInferenceDataService::getCountryNameFromAlpha2) + .filter(c -> !c.isEmpty()) + .orElseGet(() -> getCountry(ip)); + + return createThrottlingMessages( + bidderNode, + impId, + greenbidsUserAgent, + country, + hostname, + hourBucket, + minuteQuadrant); + } + + private static String getCountryNameFromAlpha2(String isoCode) { + return StringUtils.isBlank(isoCode) + ? StringUtils.EMPTY + : Locale.forLanguageTag("und-" + isoCode).getDisplayCountry(Locale.ENGLISH); + } + + private String getCountry(String ip) { + final DatabaseReader databaseReader = databaseReaderFactory.getDatabaseReader(); + return ip != null && databaseReader != null + ? getCountryFromIpUsingDatabase(databaseReader, ip) + : null; + } + + private String getCountryFromIpUsingDatabase(DatabaseReader databaseReader, String ip) { + try { + final InetAddress inetAddress = InetAddress.getByName(ip); + final CountryResponse response = databaseReader.country(inetAddress); + final Country country = response.getCountry(); + return country.getName(); + } catch (IOException | GeoIp2Exception e) { + throw new PreBidException("Failed to fetch country from geoLite DB", e); + } + } + + private List createThrottlingMessages( + JsonNode bidderNode, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final List throttlingImpMessages = new ArrayList<>(); + + if (!bidderNode.isObject()) { + return throttlingImpMessages; + } + + final ObjectNode bidders = (ObjectNode) bidderNode; + final Iterator fieldNames = bidders.fieldNames(); + while (fieldNames.hasNext()) { + final String bidderName = fieldNames.next(); + throttlingImpMessages.add(buildThrottlingMessage( + bidderName, + impId, + greenbidsUserAgent, + countryFromIp, + hostname, + hourBucket, + minuteQuadrant)); + } + + return throttlingImpMessages; + } + + private ThrottlingMessage buildThrottlingMessage( + String bidderName, + String impId, + GreenbidsUserAgent greenbidsUserAgent, + String countryFromIp, + String hostname, + Integer hourBucket, + Integer minuteQuadrant) { + + final String browser = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getBrowser) + .orElse(StringUtils.EMPTY); + + final String device = Optional.ofNullable(greenbidsUserAgent) + .map(GreenbidsUserAgent::getDevice) + .orElse(StringUtils.EMPTY); + + return ThrottlingMessage.builder() + .browser(browser) + .bidder(StringUtils.defaultString(bidderName)) + .adUnitCode(StringUtils.defaultString(impId)) + .country(StringUtils.defaultString(countryFromIp)) + .hostname(StringUtils.defaultString(hostname)) + .device(device) + .hourBucket(StringUtils.defaultString(hourBucket.toString())) + .minuteQuadrant(StringUtils.defaultString(minuteQuadrant.toString())) + .build(); + } + + private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { + try { + return mapper.treeToValue(extImpPrebid, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreator.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreator.java new file mode 100644 index 00000000000..d93f8343f22 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreator.java @@ -0,0 +1,86 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.GreenbidsConfig; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public class GreenbidsInvocationResultCreator { + + private static final int RANGE_16_BIT_INTEGER_DIVISION_BASIS = 0x10000; + private static final double DEFAULT_EXPLORATION_RATE = 1.0; + + private GreenbidsInvocationResultCreator() { + + } + + public static GreenbidsInvocationResult create(GreenbidsConfig greenbidsConfig, + BidRequest bidRequest, + Map> impsBiddersFilterMap) { + + final String greenbidsId = UUID.randomUUID().toString(); + final boolean isExploration = isExploration(greenbidsConfig, greenbidsId); + + final boolean allRejected = bidRequest.getImp().stream() + .noneMatch(imp -> impsBiddersFilterMap.get(imp.getId()).values().stream().anyMatch(isKept -> isKept)); + + final InvocationAction invocationAction = isExploration + ? InvocationAction.no_action + : allRejected + ? InvocationAction.reject + : InvocationAction.update; + + final Map ort2ImpExtResultMap = createOrtb2ImpExtForImps( + bidRequest, impsBiddersFilterMap, greenbidsId, isExploration); + final AnalyticsResult analyticsResult = AnalyticsResult.of("success", ort2ImpExtResultMap); + return GreenbidsInvocationResult.of(invocationAction, analyticsResult); + } + + private static boolean isExploration(GreenbidsConfig greenbidsConfig, String greenbidsId) { + final double explorationRate = ObjectUtils.defaultIfNull( + greenbidsConfig.getExplorationRate(), + DEFAULT_EXPLORATION_RATE); + final int hashInt = Integer.parseInt(greenbidsId.substring(greenbidsId.length() - 4), 16); + return hashInt < explorationRate * RANGE_16_BIT_INTEGER_DIVISION_BASIS; + } + + private static Map createOrtb2ImpExtForImps( + BidRequest bidRequest, + Map> impsBiddersFilterMap, + String greenbidsId, + boolean isExploration) { + + return bidRequest.getImp().stream() + .collect(Collectors.toMap( + Imp::getId, + imp -> createOrtb2ImpExt(imp, impsBiddersFilterMap, greenbidsId, isExploration))); + } + + private static Ortb2ImpExtResult createOrtb2ImpExt(Imp imp, + Map> impsBiddersFilterMap, + String greenbidsId, + boolean isExploration) { + + final String tid = Optional.ofNullable(imp) + .map(Imp::getExt) + .map(impExt -> impExt.get("tid")) + .map(JsonNode::asText) + .orElse(StringUtils.EMPTY); + final Map impBiddersFilterMap = impsBiddersFilterMap.get(imp.getId()); + final ExplorationResult explorationResult = ExplorationResult.of( + greenbidsId, impBiddersFilterMap, isExploration); + return Ortb2ImpExtResult.of(explorationResult, tid); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdater.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdater.java new file mode 100644 index 00000000000..3b1c6aa57ed --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdater.java @@ -0,0 +1,51 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class GreenbidsPayloadUpdater { + + private GreenbidsPayloadUpdater() { + + } + + public static BidRequest update(BidRequest bidRequest, Map> impsBiddersFilterMap) { + return bidRequest.toBuilder() + .imp(updateImps(bidRequest, impsBiddersFilterMap)) + .build(); + } + + private static List updateImps(BidRequest bidRequest, Map> impsBiddersFilterMap) { + return bidRequest.getImp().stream() + .filter(imp -> isImpKept(impsBiddersFilterMap.get(imp.getId()))) + .map(imp -> updateImp(imp, impsBiddersFilterMap.get(imp.getId()))) + .toList(); + } + + private static boolean isImpKept(Map bidderFilterMap) { + return bidderFilterMap.values().stream().anyMatch(isKept -> isKept); + } + + private static Imp updateImp(Imp imp, Map bidderFilterMap) { + return imp.toBuilder() + .ext(updateImpExt(imp.getExt(), bidderFilterMap)) + .build(); + } + + private static ObjectNode updateImpExt(ObjectNode impExt, Map bidderFilterMap) { + final ObjectNode updatedExt = impExt.deepCopy(); + Optional.ofNullable((ObjectNode) updatedExt.get("prebid")) + .map(prebidNode -> (ObjectNode) prebidNode.get("bidder")) + .ifPresent(bidderNode -> + bidderFilterMap.entrySet().stream() + .filter(entry -> !entry.getValue()) + .map(Map.Entry::getKey) + .forEach(bidderNode::remove)); + return updatedExt; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java new file mode 100644 index 00000000000..b7450d71560 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import ua_parser.Client; +import ua_parser.Device; +import ua_parser.OS; +import ua_parser.Parser; +import ua_parser.UserAgent; + +import java.util.Optional; +import java.util.Set; + +public class GreenbidsUserAgent { + + public static final Set PC_OS_FAMILIES = Set.of( + "Windows 95", "Windows 98", "Solaris"); + + private static final Parser UA_PARSER = new Parser(); + + private final String userAgentString; + + private final UserAgent userAgent; + + private final Device device; + + private final OS os; + + public GreenbidsUserAgent(String userAgentString) { + this.userAgentString = userAgentString; + final Client client = UA_PARSER.parse(userAgentString); + this.userAgent = client.userAgent; + this.device = client.device; + this.os = client.os; + } + + public String getDevice() { + return Optional.ofNullable(device) + .map(device -> isPC() ? "PC" : device.family) + .orElse(StringUtils.EMPTY); + } + + public String getBrowser() { + return Optional.ofNullable(userAgent) + .filter(userAgent -> !"Other".equals(userAgent.family) && StringUtils.isNoneBlank(userAgent.family)) + .map(ua -> "%s %s".formatted(ua.family, StringUtils.defaultString(userAgent.major)).trim()) + .orElse(StringUtils.EMPTY); + } + + private boolean isPC() { + final String osFamily = osFamily(); + return Optional.ofNullable(userAgentString) + .map(userAgent -> userAgent.contains("Windows NT") + || PC_OS_FAMILIES.contains(osFamily) + || ("Windows".equals(osFamily) && "ME".equals(osMajor())) + || ("Mac OS X".equals(osFamily) && !userAgent.contains("Silk")) + || (userAgent.contains("Linux") && userAgent.contains("X11"))) + .orElse(false); + } + + private String osFamily() { + return Optional.ofNullable(os).map(os -> os.family).orElse(StringUtils.EMPTY); + } + + private String osMajor() { + return Optional.ofNullable(os).map(os -> os.major).orElse(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java new file mode 100644 index 00000000000..01087287d44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java @@ -0,0 +1,96 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ModelCache { + + private static final Logger logger = LoggerFactory.getLogger(ModelCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final String onnxModelCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final OnnxModelRunnerFactory onnxModelRunnerFactory; + + public ModelCache( + Storage storage, + String gcsBucketName, + Cache cache, + String onnxModelCacheKeyPrefix, + Vertx vertx, + OnnxModelRunnerFactory onnxModelRunnerFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.onnxModelCacheKeyPrefix = Objects.requireNonNull(onnxModelCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.onnxModelRunnerFactory = Objects.requireNonNull(onnxModelRunnerFactory); + } + + public Future get(String onnxModelPath, String pbuid) { + final String cacheKey = onnxModelCacheKeyPrefix + pbuid; + final OnnxModelRunner cachedOnnxModelRunner = cache.getIfPresent(cacheKey); + + if (cachedOnnxModelRunner != null) { + return Future.succeededFuture(cachedOnnxModelRunner); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheModelRunner(onnxModelPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ModelRunner fetching in progress. Skip current request"); + } + + private Future fetchAndCacheModelRunner(String onnxModelPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(onnxModelPath)) + .map(this::loadModelRunner) + .onSuccess(onnxModelRunner -> cache.put(cacheKey, onnxModelRunner)) + .onFailure(error -> logger.error("Failed to fetch ONNX model")); + } + + private Blob getBlob(String onnxModelPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(onnxModelPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for model: ", e); + } + } + + private OnnxModelRunner loadModelRunner(Blob blob) { + try { + final byte[] onnxModelBytes = blob.getContent(); + return onnxModelRunnerFactory.create(onnxModelBytes); + } catch (OrtException e) { + throw new PreBidException("Failed to convert blob to ONNX model", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java new file mode 100644 index 00000000000..d5570f30272 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtEnvironment; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; + +import java.util.Collections; + +public class OnnxModelRunner { + + private static final OrtEnvironment ENVIRONMENT = OrtEnvironment.getEnvironment(); + + private final OrtSession session; + + public OnnxModelRunner(byte[] onnxModelBytes) throws OrtException { + session = ENVIRONMENT.createSession(onnxModelBytes, new OrtSession.SessionOptions()); + } + + public OrtSession.Result runModel(String[][] throttlingInferenceRow) throws OrtException { + final OnnxTensor inputTensor = OnnxTensor.createTensor(ENVIRONMENT, throttlingInferenceRow); + return session.run(Collections.singletonMap("input", inputTensor)); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java new file mode 100644 index 00000000000..b6082cf3e12 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; + +public class OnnxModelRunnerFactory { + + public OnnxModelRunner create(byte[] bytes) throws OrtException { + return new OnnxModelRunner(bytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java new file mode 100644 index 00000000000..eaa184f7574 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import io.vertx.core.Future; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.GreenbidsConfig; + +import java.util.Objects; + +public class OnnxModelRunnerWithThresholds { + + private final ModelCache modelCache; + + private final ThresholdCache thresholdCache; + + public OnnxModelRunnerWithThresholds( + ModelCache modelCache, + ThresholdCache thresholdCache) { + this.modelCache = Objects.requireNonNull(modelCache); + this.thresholdCache = Objects.requireNonNull(thresholdCache); + } + + public Future retrieveOnnxModelRunner(GreenbidsConfig greenbidsConfig) { + final String onnxModelPath = "models_pbuid=" + greenbidsConfig.getPbuid() + ".onnx"; + return modelCache.get(onnxModelPath, greenbidsConfig.getPbuid()); + } + + public Future retrieveThreshold(GreenbidsConfig greenbidsConfig) { + final String thresholdJsonPath = "thresholds_pbuid=" + greenbidsConfig.getPbuid() + ".json"; + return thresholdCache.get(thresholdJsonPath, greenbidsConfig.getPbuid()) + .map(greenbidsConfig::getThreshold); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java new file mode 100644 index 00000000000..44eb3d1403a --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java @@ -0,0 +1,102 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ThresholdCache { + + private static final Logger logger = LoggerFactory.getLogger(ThresholdCache.class); + + private final String gcsBucketName; + + private final Cache cache; + + private final Storage storage; + + private final ObjectMapper mapper; + + private final String thresholdsCacheKeyPrefix; + + private final AtomicBoolean isFetching; + + private final Vertx vertx; + + private final ThrottlingThresholdsFactory throttlingThresholdsFactory; + + public ThresholdCache( + Storage storage, + String gcsBucketName, + ObjectMapper mapper, + Cache cache, + String thresholdsCacheKeyPrefix, + Vertx vertx, + ThrottlingThresholdsFactory throttlingThresholdsFactory) { + this.gcsBucketName = Objects.requireNonNull(gcsBucketName); + this.cache = Objects.requireNonNull(cache); + this.storage = Objects.requireNonNull(storage); + this.mapper = Objects.requireNonNull(mapper); + this.thresholdsCacheKeyPrefix = Objects.requireNonNull(thresholdsCacheKeyPrefix); + this.isFetching = new AtomicBoolean(false); + this.vertx = Objects.requireNonNull(vertx); + this.throttlingThresholdsFactory = Objects.requireNonNull(throttlingThresholdsFactory); + } + + public Future get(String thresholdJsonPath, String pbuid) { + final String cacheKey = thresholdsCacheKeyPrefix + pbuid; + final ThrottlingThresholds cachedThrottlingThresholds = cache.getIfPresent(cacheKey); + + if (cachedThrottlingThresholds != null) { + return Future.succeededFuture(cachedThrottlingThresholds); + } + + if (isFetching.compareAndSet(false, true)) { + try { + return fetchAndCacheThrottlingThresholds(thresholdJsonPath, cacheKey); + } finally { + isFetching.set(false); + } + } + + return Future.failedFuture("ThrottlingThresholds fetching in progress. Skip current request"); + } + + private Future fetchAndCacheThrottlingThresholds(String thresholdJsonPath, String cacheKey) { + return vertx.executeBlocking(() -> getBlob(thresholdJsonPath)) + .map(this::loadThrottlingThresholds) + .onSuccess(thresholds -> cache.put(cacheKey, thresholds)) + .onFailure(error -> logger.error("Failed to fetch thresholds")); + } + + private Blob getBlob(String thresholdJsonPath) { + try { + return Optional.ofNullable(storage.get(gcsBucketName)) + .map(bucket -> bucket.get(thresholdJsonPath)) + .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName)); + } catch (StorageException e) { + throw new PreBidException("Error accessing GCS artefact for threshold: ", e); + } + } + + private ThrottlingThresholds loadThrottlingThresholds(Blob blob) { + try { + final byte[] jsonBytes = blob.getContent(); + return throttlingThresholdsFactory.create(jsonBytes, mapper); + } catch (IOException e) { + throw new PreBidException("Failed to load throttling thresholds json", e); + } + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java new file mode 100644 index 00000000000..e7ac4a6a4a9 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.io.IOException; + +public class ThrottlingThresholdsFactory { + + public ThrottlingThresholds create(byte[] bytes, ObjectMapper mapper) throws IOException { + final JsonNode thresholdsJsonNode = mapper.readTree(bytes); + return mapper.treeToValue(thresholdsJsonNode, ThrottlingThresholds.class); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/GreenbidsConfig.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/GreenbidsConfig.java new file mode 100644 index 00000000000..a57fb0e455c --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/GreenbidsConfig.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.IntStream; + +@Value(staticConstructor = "of") +public class GreenbidsConfig { + + private static final double DEFAULT_TPR = 1.0; + + String pbuid; + + @JsonProperty("target-tpr") + Double targetTpr; + + @JsonProperty("exploration-rate") + Double explorationRate; + + public Double getThreshold(ThrottlingThresholds throttlingThresholds) { + final double safeTargetTpr = targetTpr != null ? targetTpr : DEFAULT_TPR; + final List truePositiveRates = throttlingThresholds.getTpr(); + final List thresholds = throttlingThresholds.getThresholds(); + + final int minSize = Math.min(truePositiveRates.size(), thresholds.size()); + + return IntStream.range(0, minSize) + .filter(i -> truePositiveRates.get(i) >= safeTargetTpr) + .mapToObj(thresholds::get) + .max(Comparator.naturalOrder()) + .orElse(0.0); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java new file mode 100644 index 00000000000..8acb6718936 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class ThrottlingMessage { + + String browser; + + String bidder; + + String adUnitCode; + + String country; + + String hostname; + + String device; + + String hourBucket; + + String minuteQuadrant; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java new file mode 100644 index 00000000000..ccd6594ee38 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class ThrottlingThresholds { + + List thresholds; + + List tpr; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java new file mode 100644 index 00000000000..f324ac195db --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import lombok.Value; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AnalyticsResult { + + String status; + + Map values; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java new file mode 100644 index 00000000000..39e56fe5dcd --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result; + +import lombok.Value; +import org.prebid.server.hooks.v1.InvocationAction; + +@Value(staticConstructor = "of") +public class GreenbidsInvocationResult { + + InvocationAction invocationAction; + + AnalyticsResult analyticsResult; +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..f0bd5467d0e --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java @@ -0,0 +1,238 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationResultCreator; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsPayloadUpdater; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.GreenbidsConfig; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +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.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +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.stream.Collectors; +import java.util.stream.Stream; + +public class GreenbidsRealTimeDataProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String BID_REQUEST_ANALYTICS_EXTENSION_NAME = "greenbids-rtd"; + private static final String CODE = "greenbids-real-time-data-processed-auction-request"; + private static final String ACTIVITY = "greenbids-filter"; + private static final String SUCCESS_STATUS = "success"; + + private final ObjectMapper mapper; + private final FilterService filterService; + private final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds; + private final GreenbidsInferenceDataService greenbidsInferenceDataService; + + public GreenbidsRealTimeDataProcessedAuctionRequestHook( + ObjectMapper mapper, + FilterService filterService, + OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds, + GreenbidsInferenceDataService greenbidsInferenceDataService) { + + this.mapper = Objects.requireNonNull(mapper); + this.filterService = Objects.requireNonNull(filterService); + this.onnxModelRunnerWithThresholds = Objects.requireNonNull(onnxModelRunnerWithThresholds); + this.greenbidsInferenceDataService = Objects.requireNonNull(greenbidsInferenceDataService); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + final GreenbidsConfig greenbidsConfig = Optional.ofNullable(parseBidRequestExt(bidRequest.getExt())) + .orElseGet(() -> toGreenbidsConfig(invocationContext.accountConfig())); + + if (greenbidsConfig == null) { + return Future.failedFuture(new PreBidException("Greenbids config is null; cannot proceed.")); + } + + return Future.all( + onnxModelRunnerWithThresholds.retrieveOnnxModelRunner(greenbidsConfig), + onnxModelRunnerWithThresholds.retrieveThreshold(greenbidsConfig)) + .compose(compositeFuture -> toInvocationResult( + bidRequest, + greenbidsConfig, + compositeFuture.resultAt(0), + compositeFuture.resultAt(1))) + .recover(throwable -> noActionInvocationResult()); + } + + private GreenbidsConfig parseBidRequestExt(ExtRequest extRequest) { + return Optional.ofNullable(extRequest) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .filter(this::isNotEmptyObjectNode) + .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME)) + .map(this::toGreenbidsConfig) + .orElse(null); + } + + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private GreenbidsConfig toGreenbidsConfig(ObjectNode greenbidsConfigNode) { + try { + return mapper.treeToValue(greenbidsConfigNode, GreenbidsConfig.class); + } catch (JsonProcessingException e) { + return null; + } + } + + private Future> toInvocationResult( + BidRequest bidRequest, + GreenbidsConfig greenbidsConfig, + OnnxModelRunner onnxModelRunner, + Double threshold) { + + final Map> impsBiddersFilterMap; + try { + final List throttlingMessages = greenbidsInferenceDataService + .extractThrottlingMessagesFromBidRequest(bidRequest); + + impsBiddersFilterMap = filterService.filterBidders( + onnxModelRunner, + throttlingMessages, + threshold); + } catch (PreBidException e) { + return noActionInvocationResult(); + } + + final GreenbidsInvocationResult invocationResult = GreenbidsInvocationResultCreator.create( + greenbidsConfig, + bidRequest, + impsBiddersFilterMap); + + return invocationResult.getInvocationAction() == InvocationAction.no_action + ? noActionInvocationResult(invocationResult.getAnalyticsResult()) + : toInvocationResult(bidRequest, impsBiddersFilterMap, invocationResult); + } + + private Future> toInvocationResult( + BidRequest bidRequest, + Map> impsBiddersFilterMap, + GreenbidsInvocationResult invocationResult) { + + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(invocationResult.getInvocationAction()) + .payloadUpdate(payload -> AuctionRequestPayloadImpl.of( + GreenbidsPayloadUpdater.update(bidRequest, impsBiddersFilterMap))) + .analyticsTags(toAnalyticsTags(invocationResult.getAnalyticsResult())) + .rejections(toRejections(impsBiddersFilterMap)) + .build()); + } + + private Future> noActionInvocationResult(AnalyticsResult analyticsResult) { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(toAnalyticsTags(analyticsResult)) + .build()); + } + + private Future> noActionInvocationResult() { + return noActionInvocationResult(null); + } + + private Tags toAnalyticsTags(AnalyticsResult analyticsResult) { + if (analyticsResult == null) { + return null; + } + + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY, + SUCCESS_STATUS, + toResults(analyticsResult)))); + } + + private List toResults(AnalyticsResult analyticsResult) { + return analyticsResult.getValues().entrySet().stream() + .map(entry -> toResult(analyticsResult.getStatus(), entry)) + .toList(); + } + + private Result toResult(String status, Map.Entry entry) { + final String impId = entry.getKey(); + final Ortb2ImpExtResult ortb2ImpExtResult = entry.getValue(); + final List removedBidders = Optional.ofNullable(ortb2ImpExtResult) + .map(Ortb2ImpExtResult::getGreenbids) + .map(ExplorationResult::getKeptInAuction) + .map(Map::entrySet) + .stream() + .flatMap(Collection::stream) + .filter(e -> BooleanUtils.isFalse(e.getValue())) + .map(Map.Entry::getKey) + .toList(); + + return ResultImpl.of( + status, + toObjectNode(entry), + AppliedToImpl.builder() + .impIds(Collections.singletonList(impId)) + .bidders(removedBidders.isEmpty() ? null : removedBidders) + .build()); + } + + private ObjectNode toObjectNode(Map.Entry values) { + return values != null ? mapper.valueToTree(values) : null; + } + + private Map> toRejections(Map> impsBiddersFilterMap) { + return impsBiddersFilterMap.entrySet().stream() + .flatMap(entry -> Stream.ofNullable(entry.getValue()) + .map(Map::entrySet) + .flatMap(Collection::stream) + .filter(e -> BooleanUtils.isFalse(e.getValue())) + .map(Map.Entry::getKey) + .map(bidder -> Pair.of( + bidder, + ImpRejection.of(entry.getKey(), BidRejectionReason.REQUEST_BLOCKED_OPTIMIZED)))) + .collect(Collectors.groupingBy(Pair::getKey, Collectors.mapping(Pair::getValue, Collectors.toList()))); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java new file mode 100644 index 00000000000..0a3ab82b9cd --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java @@ -0,0 +1,177 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OnnxValue; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import ai.onnxruntime.TensorInfo; +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.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Arrays; +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.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class FilterServiceTest { + + @Mock + private OnnxModelRunner onnxModelRunnerMock; + + @Mock + private OrtSession.Result results; + + @Mock + private OnnxTensor onnxTensor; + + @Mock + private TensorInfo tensorInfo; + + @Mock + private OnnxValue onnxValue; + + private final FilterService target = new FilterService(); + + @Test + public void filterBiddersShouldReturnFilteredBiddersWhenValidThrottlingMessagesProvided() + throws OrtException, IOException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + final OnnxModelRunner onnxModelRunner = givenOnnxModelRunner(); + + // when + final Map> impsBiddersFilterMap = target.filterBidders( + onnxModelRunner, throttlingMessages, threshold); + + // then + assertThat(impsBiddersFilterMap).isNotNull(); + assertThat(impsBiddersFilterMap.get("adUnit1").get("bidder1")).isTrue(); + assertThat(impsBiddersFilterMap.get("adUnit2").get("bidder2")).isFalse(); + assertThat(impsBiddersFilterMap.get("adUnit3").get("bidder3")).isFalse(); + } + + @Test + public void validateOnnxTensorShouldThrowPreBidExceptionWhenOnnxValueIsNotTensor() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createInvalidOnnxItem()).spliterator()); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Expected OnnxTensor for 'probabilities', but found"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenOrtExceptionOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))) + .thenThrow(new OrtException("Exception during runModel")); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Exception during model inference"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenThrottlingMessagesIsEmpty() { + // given + final List throttlingMessages = Collections.emptyList(); + final Double threshold = 0.5; + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("throttlingMessages cannot be null or empty"); + } + + @Test + public void filterBiddersShouldThrowPreBidExceptionWhenTensorSizeMismatchOccurs() throws OrtException { + // given + final List throttlingMessages = createThrottlingMessages(); + final Double threshold = 0.5; + + when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results); + when(results.spliterator()).thenReturn(Arrays.asList(createOnnxItem()).spliterator()); + when(onnxTensor.getInfo()).thenReturn(tensorInfo); + when(tensorInfo.getShape()).thenReturn(new long[]{0}); + + // when & then + assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Mismatch between tensor size and throttlingMessages size"); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } + + private List createThrottlingMessages() { + final ThrottlingMessage throttlingMessage1 = ThrottlingMessage.builder() + .browser("Chrome") + .bidder("bidder1") + .adUnitCode("adUnit1") + .country("US") + .hostname("localhost") + .device("PC") + .hourBucket("10") + .minuteQuadrant("1") + .build(); + + final ThrottlingMessage throttlingMessage2 = ThrottlingMessage.builder() + .browser("Firefox") + .bidder("bidder2") + .adUnitCode("adUnit2") + .country("FR") + .hostname("www.leparisien.fr") + .device("Mobile") + .hourBucket("11") + .minuteQuadrant("2") + .build(); + + final ThrottlingMessage throttlingMessage3 = ThrottlingMessage.builder() + .browser("Safari") + .bidder("bidder3") + .adUnitCode("adUnit3") + .country("FR") + .hostname("www.lesechos.fr") + .device("Tablet") + .hourBucket("12") + .minuteQuadrant("3") + .build(); + + return Arrays.asList(throttlingMessage1, throttlingMessage2, throttlingMessage3); + } + + private Map.Entry createOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxTensor); + } + + private Map.Entry createInvalidOnnxItem() { + return new AbstractMap.SimpleEntry<>("probabilities", onnxValue); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java new file mode 100644 index 00000000000..1a93b22a502 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java @@ -0,0 +1,202 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +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.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.Country; +import org.apache.commons.lang3.StringUtils; +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.exception.PreBidException; +import org.prebid.server.geolocation.CountryCodeMapper; +import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.net.InetAddress; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; + +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.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInferenceDataServiceTest { + + @Mock(strictness = LENIENT) + private DatabaseReaderFactory databaseReaderFactory; + + @Mock + private DatabaseReader databaseReader; + + @Mock + private Country country; + + @Mock + private CountryCodeMapper countryCodeMapper; + + private GreenbidsInferenceDataService target; + + @BeforeEach + public void setUp() { + when(databaseReaderFactory.getDatabaseReader()).thenReturn(databaseReader); + target = new GreenbidsInferenceDataService( + databaseReaderFactory, TestBidRequestProvider.MAPPER, countryCodeMapper); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldReturnValidThrottlingMessagesWhenGeoIsNull() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp)); + + final CountryResponse countryResponse = mock(CountryResponse.class); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + when(databaseReader.country(any(InetAddress.class))).thenReturn(countryResponse); + when(countryResponse.getCountry()).thenReturn(country); + when(country.getName()).thenReturn("United States"); + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + assertThat(throttlingMessages) + .extracting(ThrottlingMessage::getBidder) + .containsExactly("rubicon", "appnexus", "pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo("United States"); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldReturnValidThrottlingMessagesWhenGeoDefined() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final BidRequest bidRequest = givenBidRequest( + request -> request.device(givenDevice("FRA")), + List.of(imp)); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + when(countryCodeMapper.mapToAlpha2("FRA")).thenReturn("FR"); + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + assertThat(throttlingMessages) + .extracting(ThrottlingMessage::getBidder) + .containsExactly("rubicon", "appnexus", "pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo("France"); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldHandleMissingIp() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final BidRequest bidRequest = givenBidRequest(request -> request.device(givenDeviceWithoutIp()), List.of(imp)); + + final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC")); + final Integer expectedHourBucket = timestamp.getHour(); + final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1; + + // when + final List throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest); + + // then + assertThat(throttlingMessages).isNotEmpty(); + assertThat(throttlingMessages) + .extracting(ThrottlingMessage::getBidder) + .containsExactly("rubicon", "appnexus", "pubmatic"); + + throttlingMessages.forEach(message -> { + assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue"); + assertThat(message.getCountry()).isEqualTo(StringUtils.EMPTY); + assertThat(message.getHostname()).isEqualTo("www.leparisien.fr"); + assertThat(message.getDevice()).isEqualTo("PC"); + assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket)); + assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant)); + }); + } + + @Test + public void extractThrottlingMessagesFromBidRequestShouldThrowPreBidExceptionWhenGeoIpFails() + throws IOException, GeoIp2Exception { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + final BidRequest bidRequest = givenBidRequest(request -> request.device(givenDevice()), List.of(imp)); + + when(databaseReader.country(any(InetAddress.class))).thenThrow(new GeoIp2Exception("GeoIP failure")); + + // when & then + assertThatThrownBy(() -> target.extractThrottlingMessagesFromBidRequest(bidRequest)) + .isInstanceOf(PreBidException.class) + .hasMessageContaining("Failed to fetch country from geoLite DB"); + } + + private Device givenDeviceWithoutIp() { + final String userAgent = "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"; + return Device.builder().ua(userAgent).build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreatorTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreatorTest.java new file mode 100644 index 00000000000..1aa9e5022b4 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationResultCreatorTest.java @@ -0,0 +1,136 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.iab.openrtb.request.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.GreenbidsConfig; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; +import org.prebid.server.hooks.v1.InvocationAction; + +import java.util.List; +import java.util.Map; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsInvocationResultCreatorTest { + + @Test + public void createGreenbidsInvocationResultWhenNotExploration() { + // given + final Banner banner = givenBanner(); + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .banner(banner) + .build(); + + final BidRequest bidRequest = givenBidRequest(identity(), List.of(imp)); + final Map> impsBiddersFilterMap = givenImpsBiddersFilterMap(); + final GreenbidsConfig greenbidsConfig = givenConfig(0.0); + + // when + final GreenbidsInvocationResult result = GreenbidsInvocationResultCreator.create( + greenbidsConfig, bidRequest, impsBiddersFilterMap); + + // then + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.update); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isFalse(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isTrue(); + assertThat(keptInAuction.get("appnexus")).isFalse(); + assertThat(keptInAuction.get("pubmatic")).isFalse(); + } + + @Test + public void createShouldReturnNoActionWhenExploration() { + // given + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .build(); + + final BidRequest bidRequest = givenBidRequest(identity(), List.of(imp)); + final Map> impsBiddersFilterMap = givenFilterMapWithAllFilteredImps(); + final GreenbidsConfig greenbidsConfig = givenConfig(1.0); + + // when + final GreenbidsInvocationResult result = GreenbidsInvocationResultCreator.create( + greenbidsConfig, bidRequest, impsBiddersFilterMap); + + // then + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.no_action); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isTrue(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isFalse(); + assertThat(keptInAuction.get("appnexus")).isFalse(); + assertThat(keptInAuction.get("pubmatic")).isFalse(); + } + + @Test + public void createShouldReturnRejectWhenAllImpsAreFilteredOutAndNoExploration() { + // given + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .build(); + + final BidRequest bidRequest = givenBidRequest(identity(), List.of(imp)); + final Map> impsBiddersFilterMap = givenFilterMapWithAllFilteredImps(); + final GreenbidsConfig greenbidsConfig = givenConfig(0.001); + + // when + final GreenbidsInvocationResult result = GreenbidsInvocationResultCreator.create( + greenbidsConfig, bidRequest, impsBiddersFilterMap); + + // then + final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue"); + final Map keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction(); + + assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.reject); + assertThat(ortb2ImpExtResult).isNotNull(); + assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isFalse(); + assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull(); + assertThat(keptInAuction.get("rubicon")).isFalse(); + assertThat(keptInAuction.get("appnexus")).isFalse(); + assertThat(keptInAuction.get("pubmatic")).isFalse(); + } + + private Map> givenImpsBiddersFilterMap() { + final Map biddersFitlerMap = Map.of( + "rubicon", true, + "appnexus", false, + "pubmatic", false); + + return Map.of("adunitcodevalue", biddersFitlerMap); + } + + private Map> givenFilterMapWithAllFilteredImps() { + final Map biddersFitlerMap = Map.of( + "rubicon", false, + "appnexus", false, + "pubmatic", false); + + return Map.of("adunitcodevalue", biddersFitlerMap); + } + + private GreenbidsConfig givenConfig(Double explorationRate) { + return GreenbidsConfig.of("test-pbuid", 0.60, explorationRate); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdaterTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdaterTest.java new file mode 100644 index 00000000000..36ed3f8e980 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsPayloadUpdaterTest.java @@ -0,0 +1,60 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.getAppnexusNode; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.getPubmaticNode; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.getRubiconNode; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +public class GreenbidsPayloadUpdaterTest { + + @Test + public void updateShouldReturnUpdatedBidRequest() { + // given + final Imp givenImp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt(getRubiconNode(), getAppnexusNode(), getPubmaticNode())) + .build(); + + // when + final BidRequest result = GreenbidsPayloadUpdater.update( + givenBidRequest(identity(), List.of(givenImp)), + Map.of("adunitcodevalue", Map.of("rubicon", true, "appnexus", false, "pubmatic", false))); + + // then + final Imp expectedImp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt(getRubiconNode(), null, null)) + .build(); + + assertThat(result.getImp()).containsOnly(expectedImp); + } + + @Test + public void updateShouldRemoveImpFromUpdateBidRequestWhenAllBiddersFiltered() { + // given + final Imp givenImp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt(getRubiconNode(), null, null)) + .build(); + + // when + final BidRequest result = GreenbidsPayloadUpdater.update( + givenBidRequest(identity(), List.of(givenImp)), + Map.of("adunitcodevalue", Map.of("rubicon", false, "appnexus", false, "pubmatic", false))); + + // then + assertThat(result.getImp()).isEmpty(); + + } + +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java new file mode 100644 index 00000000000..b4839146a44 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java @@ -0,0 +1,59 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GreenbidsUserAgentTest { + + @Test + public void getDeviceShouldReturnPCWhenWindowsNTInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("PC"); + } + + @Test + public void getDeviceShouldReturnDeviceIPhoneWhenIOSInUserAgent() { + // given + final String userAgentString = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getDevice()).isEqualTo("iPhone"); + } + + @Test + public void getBrowserShouldReturnBrowserNameAndVersionWhenUserAgentIsPresent() { + // given + final String userAgentString = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + " (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo("Chrome 58"); + } + + @Test + public void getBrowserShouldReturnEmptyStringWhenBrowserIsNull() { + // given + final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"; + + // when + final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString); + + // then + assertThat(greenbidsUserAgent.getBrowser()).isEqualTo(StringUtils.EMPTY); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java new file mode 100644 index 00000000000..2ceeaa14e43 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java @@ -0,0 +1,191 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OrtException; +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +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.exception.PreBidException; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ModelCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String MODEL_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String ONNX_MODEL_PATH = "model.onnx"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private OnnxModelRunner onnxModelRunner; + + @Mock(strictness = LENIENT) + private OnnxModelRunnerFactory onnxModelRunnerFactory; + + @Mock + private ModelCache target; + + private Vertx vertx; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ModelCache( + storage, GCS_BUCKET_NAME, cache, MODEL_CACHE_KEY_PREFIX, vertx, onnxModelRunnerFactory); + } + + @Test + public void getShouldReturnModelFromCacheWhenPresent() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(onnxModelRunner); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + final ModelCache spyModelCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + final Field isFetchingField = ModelCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyModelCache, mockFetchingState); + + // when + final Future result = spyModelCache.get(ONNX_MODEL_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ModelRunner fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchModelWhenNotInCache() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenReturn(onnxModelRunner); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(onnxModelRunner); + verify(cache).put(eq(cacheKey), eq(onnxModelRunner)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenOnnxModelFails() throws OrtException { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + final byte[] bytes = new byte[]{1, 2, 3}; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(onnxModelRunnerFactory.create(bytes)).thenThrow( + new OrtException("Failed to convert blob to ONNX model")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to convert blob to ONNX model"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID; + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(ONNX_MODEL_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } + +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java new file mode 100644 index 00000000000..51c4b34ff91 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java @@ -0,0 +1,73 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import ai.onnxruntime.OnnxTensor; +import ai.onnxruntime.OrtException; +import ai.onnxruntime.OrtSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; +import java.util.Objects; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class OnnxModelRunnerTest { + + private OnnxModelRunner target; + + @BeforeEach + public void setUp() throws OrtException, IOException { + target = givenOnnxModelRunner(); + } + + @Test + public void runModelShouldReturnProbabilitiesWhenValidThrottlingInferenceRow() throws OrtException { + // given + final String[][] throttlingInferenceRow = {{ + "Chrome 59", "rubicon", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when + final OrtSession.Result actualResult = target.runModel(throttlingInferenceRow); + + // then + final float[][] probabilities = StreamSupport.stream(actualResult.spliterator(), false) + .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities")) + .map(Map.Entry::getValue) + .map(OnnxTensor.class::cast) + .map(tensor -> { + try { + return (float[][]) tensor.getValue(); + } catch (OrtException e) { + throw new RuntimeException(e); + } + }).findFirst().get(); + + assertThat(actualResult).isNotNull(); + assertThat(actualResult).hasSize(2); + assertThat(probabilities[0]).isNotEmpty(); + assertThat(probabilities[0][0]).isBetween(0.0f, 1.0f); + assertThat(probabilities[0][1]).isBetween(0.0f, 1.0f); + } + + @Test + public void runModelShouldThrowOrtExceptionWhenNonValidThrottlingInferenceRow() { + // given + final String[][] throttlingInferenceRowWithMissingColumn = {{ + "Chrome 59", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}}; + + // when & then + assertThatThrownBy(() -> target.runModel(throttlingInferenceRowWithMissingColumn)) + .isInstanceOf(OrtException.class); + } + + private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException { + final byte[] onnxModelBytes = Files.readAllBytes(Paths.get( + "src/test/resources/models_pbuid=test-pbuid.onnx")); + return new OnnxModelRunner(onnxModelBytes); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java new file mode 100644 index 00000000000..90a8d521f71 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java @@ -0,0 +1,198 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.core; + +import com.github.benmanes.caffeine.cache.Cache; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageException; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +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.exception.PreBidException; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ThresholdCacheTest { + + private static final String GCS_BUCKET_NAME = "test_bucket"; + private static final String THRESHOLD_CACHE_KEY_PREFIX = "onnxModelRunner_"; + private static final String PBUUID = "test-pbuid"; + private static final String THRESHOLDS_PATH = "thresholds.json"; + + @Mock + private Cache cache; + + @Mock(strictness = LENIENT) + private Storage storage; + + @Mock(strictness = LENIENT) + private Bucket bucket; + + @Mock(strictness = LENIENT) + private Blob blob; + + @Mock + private ThrottlingThresholds throttlingThresholds; + + @Mock(strictness = LENIENT) + private ThrottlingThresholdsFactory throttlingThresholdsFactory; + + private Vertx vertx; + + private ThresholdCache target; + + @BeforeEach + public void setUp() { + vertx = Vertx.vertx(); + target = new ThresholdCache( + storage, + GCS_BUCKET_NAME, + TestBidRequestProvider.MAPPER, + cache, + THRESHOLD_CACHE_KEY_PREFIX, + vertx, + throttlingThresholdsFactory); + } + + @Test + public void getShouldReturnThresholdsFromCacheWhenPresent() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(future.result()).isEqualTo(throttlingThresholds); + verify(cache).getIfPresent(eq(cacheKey)); + } + + @Test + public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + + final ThresholdCache spyThresholdCache = spy(target); + final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(mockFetchingState.compareAndSet(false, true)).thenReturn(false); + + final Field isFetchingField = ThresholdCache.class.getDeclaredField("isFetching"); + isFetchingField.setAccessible(true); + isFetchingField.set(spyThresholdCache, mockFetchingState); + + // when + final Future result = spyThresholdCache.get(THRESHOLDS_PATH, PBUUID); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause().getMessage()).isEqualTo( + "ThrottlingThresholds fetching in progress. Skip current request"); + } + + @Test + public void getShouldFetchThresholdsWhenNotInCache() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)) + .thenReturn(throttlingThresholds); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.succeeded()).isTrue(); + assertThat(ar.result()).isEqualTo(throttlingThresholds); + verify(cache).put(eq(cacheKey), eq(throttlingThresholds)); + }); + } + + @Test + public void getShouldThrowExceptionWhenStorageFails() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for threshold"); + }); + } + + @Test + public void getShouldThrowExceptionWhenLoadingJsonFails() throws IOException { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + final String jsonContent = "test_json_content"; + final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8); + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenReturn(bytes); + when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)).thenThrow( + new IOException("Failed to load throttling thresholds json")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Failed to load throttling thresholds json"); + }); + } + + @Test + public void getShouldThrowExceptionWhenBucketNotFound() { + // given + final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID; + when(cache.getIfPresent(eq(cacheKey))).thenReturn(null); + when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket); + when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob); + when(blob.getContent()).thenThrow(new PreBidException("Bucket not found")); + + // when + final Future future = target.get(THRESHOLDS_PATH, PBUUID); + + // then + future.onComplete(ar -> { + assertThat(ar.failed()).isTrue(); + assertThat(ar.cause()).isInstanceOf(PreBidException.class); + assertThat(ar.cause().getMessage()).contains("Bucket not found"); + }); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java new file mode 100644 index 00000000000..9a33620c087 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java @@ -0,0 +1,105 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.Format; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Site; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.Collections; +import java.util.List; +import java.util.function.UnaryOperator; + +public class TestBidRequestProvider { + + public static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + private TestBidRequestProvider() { } + + public static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer, + List imps) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("request") + .imp(imps) + .site(givenSite()) + .device(givenDevice())) + .build(); + } + + public static Site givenSite() { + return Site.builder().domain("www.leparisien.fr").build(); + } + + public static ObjectNode givenImpExt() { + return givenImpExt(getRubiconNode(), getAppnexusNode(), getPubmaticNode()); + } + + public static ObjectNode givenImpExt(ObjectNode rubiconNode, ObjectNode appnexusNode, ObjectNode pubmaticNode) { + final ObjectNode bidderNode = MAPPER.createObjectNode(); + + if (rubiconNode != null) { + bidderNode.set("rubicon", rubiconNode); + } + + if (appnexusNode != null) { + bidderNode.set("appnexus", appnexusNode); + } + + if (pubmaticNode != null) { + bidderNode.set("pubmatic", pubmaticNode); + } + + return MAPPER.createObjectNode() + .put("tid", "67eaab5f-27a6-4689-93f7-bd8f024576e3") + .set("prebid", MAPPER.createObjectNode().set("bidder", bidderNode)); + } + + public static ObjectNode getPubmaticNode() { + return MAPPER.createObjectNode() + .put("publisherId", "156209") + .put("adSlot", "slot1@300x250"); + } + + public static ObjectNode getAppnexusNode() { + return MAPPER.createObjectNode().put("placementId", 123456); + } + + public static ObjectNode getRubiconNode() { + return MAPPER.createObjectNode() + .put("accountId", 1001) + .put("siteId", 267318) + .put("zoneId", 1861698); + } + + public static Device givenDevice(String countryAlpha3) { + final String userAgent = "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"; + final Geo geo = Geo.builder().country(countryAlpha3).build(); + return Device.builder().ua(userAgent).ip("151.101.194.216").geo(geo).build(); + } + + public static Device givenDevice() { + final String userAgent = "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"; + return Device.builder().ua(userAgent).ip("151.101.194.216").build(); + } + + public static Banner givenBanner() { + final Format format = Format.builder() + .w(320) + .h(50) + .build(); + + return Banner.builder() + .format(Collections.singletonList(format)) + .w(240) + .h(400) + .build(); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..42e16253445 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java @@ -0,0 +1,189 @@ +package org.prebid.server.hooks.modules.greenbids.real.time.data.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +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.ImpRejection; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +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.analytics.AppliedTo; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.prebid.server.auction.model.BidRejectionReason.REQUEST_BLOCKED_OPTIMIZED; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.MAPPER; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.getRubiconNode; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest; +import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt; + +@ExtendWith(MockitoExtension.class) +public class GreenbidsRealTimeDataProcessedAuctionRequestHookTest { + + @Mock + private FilterService filterService; + + @Mock + private OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds; + + @Mock + private GreenbidsInferenceDataService greenbidsInferenceDataService; + + private GreenbidsRealTimeDataProcessedAuctionRequestHook target; + + @BeforeEach + public void setUp() { + given(onnxModelRunnerWithThresholds.retrieveOnnxModelRunner(any())) + .willReturn(Future.succeededFuture(mock(OnnxModelRunner.class))); + given(onnxModelRunnerWithThresholds.retrieveThreshold(any())) + .willReturn(Future.succeededFuture(18.2d)); + given(greenbidsInferenceDataService.extractThrottlingMessagesFromBidRequest(any())) + .willReturn(Collections.emptyList()); + + target = new GreenbidsRealTimeDataProcessedAuctionRequestHook( + MAPPER, + filterService, + onnxModelRunnerWithThresholds, + greenbidsInferenceDataService); + } + + @Test + public void callShouldReturnAnalyticTagsWithoutFilteringOutBiddersWhenExplorationIsTrue() { + // given + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .build(); + + final Double explorationRate = 1.0; + final BidRequest bidRequest = givenBidRequest(identity(), List.of(imp)); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(explorationRate); + + given(filterService.filterBidders(any(), any(), any())).willReturn(Map.of("adunitcodevalue", + Map.of("rubicon", false, "appnexus", false, "pubmatic", false))); + + // when + final Future> future = target + .call(AuctionRequestPayloadImpl.of(bidRequest), invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + + final ActivityImpl actualActivity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl actualResult = (ResultImpl) actualActivity.results().getFirst(); + final AppliedTo acctualAppliedTo = actualResult.appliedTo(); + + assertThat(acctualAppliedTo.bidders()).containsOnly("appnexus", "pubmatic", "rubicon"); + assertThat(acctualAppliedTo.impIds()).containsOnly("adunitcodevalue"); + assertThat(actualResult.values().get("adunitcodevalue").get("greenbids").get("keptInAuction")) + .isEqualTo(MAPPER.createObjectNode() + .put("rubicon", false) + .put("appnexus", false) + .put("pubmatic", false)); + assertThat(actualResult.values().get("adunitcodevalue").get("greenbids").get("fingerprint").asText()) + .isNotNull(); + assertThat(actualResult.values().get("adunitcodevalue").get("tid").asText()) + .isEqualTo("67eaab5f-27a6-4689-93f7-bd8f024576e3"); + assertThat(result.rejections()).isNull(); + } + + @Test + public void callShouldFilterBiddersBasedOnModelResultsWhenExplorationIsFalse() { + // given + final Imp imp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt()) + .build(); + + final Double explorationRate = 0.0001; + final BidRequest bidRequest = givenBidRequest(identity(), List.of(imp)); + final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(explorationRate); + + given(filterService.filterBidders(any(), any(), any())).willReturn(Map.of("adunitcodevalue", + Map.of("rubicon", true, "appnexus", false, "pubmatic", false))); + + // when + final Future> future = target + .call(AuctionRequestPayloadImpl.of(bidRequest), invocationContext); + final InvocationResult result = future.result(); + final BidRequest resultBidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(bidRequest)) + .bidRequest(); + + // then + assertThat(future.succeeded()).isTrue(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + + final Imp expectedImp = Imp.builder() + .id("adunitcodevalue") + .ext(givenImpExt(getRubiconNode(), null, null)) + .build(); + assertThat(resultBidRequest).isEqualTo(givenBidRequest(identity(), List.of(expectedImp))); + + final ActivityImpl actualActivity = (ActivityImpl) result.analyticsTags().activities().getFirst(); + final ResultImpl actualResult = (ResultImpl) actualActivity.results().getFirst(); + final AppliedTo acctualAppliedTo = actualResult.appliedTo(); + + assertThat(acctualAppliedTo.bidders()).containsOnly("appnexus", "pubmatic"); + assertThat(acctualAppliedTo.impIds()).containsOnly("adunitcodevalue"); + assertThat(actualResult.values().get("adunitcodevalue").get("greenbids").get("keptInAuction")) + .isEqualTo(MAPPER.createObjectNode() + .put("rubicon", true) + .put("appnexus", false) + .put("pubmatic", false)); + assertThat(actualResult.values().get("adunitcodevalue").get("greenbids").get("fingerprint").asText()) + .isNotNull(); + assertThat(actualResult.values().get("adunitcodevalue").get("tid").asText()) + .isEqualTo("67eaab5f-27a6-4689-93f7-bd8f024576e3"); + assertThat(result.rejections()).containsOnly( + entry("appnexus", List.of(ImpRejection.of("adunitcodevalue", REQUEST_BLOCKED_OPTIMIZED))), + entry("pubmatic", List.of(ImpRejection.of("adunitcodevalue", REQUEST_BLOCKED_OPTIMIZED)))); + } + + private AuctionInvocationContext givenAuctionInvocationContext(Double explorationRate) { + return AuctionInvocationContextImpl.of( + null, + null, + false, + givenAccountConfig(explorationRate), + null); + } + + private ObjectNode givenAccountConfig(Double explorationRate) { + final ObjectNode greenbidsNode = MAPPER.createObjectNode(); + greenbidsNode.put("enabled", true); + greenbidsNode.put("pbuid", "test-pbuid"); + greenbidsNode.put("target-tpr", 0.99); + greenbidsNode.put("exploration-rate", explorationRate); + return greenbidsNode; + } +} diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx new file mode 100644 index 00000000000..f0acc8c66fe Binary files /dev/null and b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx differ diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json new file mode 100644 index 00000000000..462a6459297 --- /dev/null +++ b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json @@ -0,0 +1,14 @@ +{ + "thresholds": [ + 0.4, + 0.224, + 0.018, + 0.018 + ], + "tpr": [ + 0.8, + 0.95, + 0.99, + 0.9999 + ] +} diff --git a/extra/modules/live-intent-omni-channel-identity/README.md b/extra/modules/live-intent-omni-channel-identity/README.md new file mode 100644 index 00000000000..be5ad801ec1 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/README.md @@ -0,0 +1,46 @@ +# Overview + +This module enriches bid requests with user EIDs. + +The user EIDs to be enriched are configured per partner as part of the LiveIntent HIRO onboarding process. As part of this onboarding process, partners will also be provided with the `identity-resolution-endpoint` URL as well as with the `auth-token`. + +`treatment-rate` is a value between 0.0 and 1.0 (including 0.0 and 1.0) and defines the percentage of requests for which identity enrichment should be performed. This value can be freely picked. We recommend a value between 0.9 and 0.95 + +## Configuration + +To start using the LiveIntent Omni Channel Identity module you have to enable it and add configuration: + +```yaml +hooks: + liveintent-omni-channel-identity: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 100, + "hook-sequence": [ + { + "module-code": "liveintent-omni-channel-identity", + "hook-impl-code": "liveintent-omni-channel-identity-enrichment-hook" + } + ] + } + ] + } + } + } + } + } + modules: + liveintent-omni-channel-identity: + request-timeout-ms: 2000 + identity-resolution-endpoint: "https://liveintent.com/idx" + auth-token: "secret-token" + treatment-rate: 0.9 +``` + diff --git a/extra/modules/live-intent-omni-channel-identity/pom.xml b/extra/modules/live-intent-omni-channel-identity/pom.xml new file mode 100644 index 00000000000..c0c6c463354 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + live-intent-omni-channel-identity + + live-intent-omni-channel-identity + LiveIntent Omni-Channel Identity + + + + diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java new file mode 100644 index 00000000000..d41a4843e5b --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/config/LiveIntentOmniChannelIdentityConfiguration.java @@ -0,0 +1,44 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.config; + +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.vertx.httpclient.HttpClient; +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 java.util.Collections; + +@Configuration +@ConditionalOnProperty( + prefix = "hooks." + LiveIntentOmniChannelIdentityModule.CODE, + name = "enabled", + havingValue = "true") +public class LiveIntentOmniChannelIdentityConfiguration { + + @Bean + @ConfigurationProperties(prefix = "hooks.modules." + LiveIntentOmniChannelIdentityModule.CODE) + LiveIntentOmniChannelProperties liveIntentOmniChannelProperties() { + return new LiveIntentOmniChannelProperties(); + } + + @Bean + Module liveIntentOmniChannelIdentityModule(LiveIntentOmniChannelProperties liveIntentOmniChannelProperties, + JacksonMapper mapper, + UserFpdActivityMask userFpdActivityMask, + HttpClient httpClient, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + final LiveIntentOmniChannelIdentityProcessedAuctionRequestHook hook = + new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + liveIntentOmniChannelProperties, userFpdActivityMask, mapper, httpClient, logSamplingRate); + + return new LiveIntentOmniChannelIdentityModule(Collections.singleton(hook)); + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java new file mode 100644 index 00000000000..35b22adca0d --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/IdResResponse.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model; + +import com.iab.openrtb.request.Eid; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor(staticName = "of") +public class IdResResponse { + + List eids; +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java new file mode 100644 index 00000000000..01d37eabda0 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/model/config/LiveIntentOmniChannelProperties.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config; + +import lombok.Data; + +import java.util.List; + +@Data +public final class LiveIntentOmniChannelProperties { + + long requestTimeoutMs; + + String identityResolutionEndpoint; + + String authToken; + + float treatmentRate; + + List targetBidders; +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java new file mode 100644 index 00000000000..0ffdce8b436 --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityModule.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.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 LiveIntentOmniChannelIdentityModule( + Collection> hooks) implements Module { + + public static final String CODE = "liveintent-omni-channel-identity"; + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..32b608ce21c --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/main/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/hooks/LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.java @@ -0,0 +1,263 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +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.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.LiveIntentOmniChannelIdentityModule; +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.ProcessedAuctionRequestHook; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.log.ConditionalLogger; +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.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.util.StreamUtil; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(LoggerFactory.getLogger( + LiveIntentOmniChannelIdentityProcessedAuctionRequestHook.class)); + + private static final String CODE = "liveintent-omni-channel-identity-enrichment-hook"; + + private final LiveIntentOmniChannelProperties config; + private final JacksonMapper mapper; + private final HttpClient httpClient; + private final UserFpdActivityMask userFpdActivityMask; + private final double logSamplingRate; + private final List targetBidders; + + public LiveIntentOmniChannelIdentityProcessedAuctionRequestHook(LiveIntentOmniChannelProperties config, + UserFpdActivityMask userFpdActivityMask, + JacksonMapper mapper, + HttpClient httpClient, + double logSamplingRate) { + + this.config = Objects.requireNonNull(config); + HttpUtil.validateUrlSyntax(config.getIdentityResolutionEndpoint()); + this.mapper = Objects.requireNonNull(mapper); + this.httpClient = Objects.requireNonNull(httpClient); + this.logSamplingRate = logSamplingRate; + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.targetBidders = ListUtils.emptyIfNull(config.getTargetBidders()); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + return config.getTreatmentRate() > ThreadLocalRandom.current().nextFloat() + ? requestIdentities(auctionRequestPayload.bidRequest(), invocationContext.auctionContext()) + .>map(this::update) + .onFailure(throwable -> conditionalLogger.error( + "Failed enrichment: %s".formatted(throwable.getMessage()), logSamplingRate)) + : noAction(); + } + + private Future requestIdentities(BidRequest bidRequest, AuctionContext auctionContext) { + final BidRequest restrictedBidRequest = applyActivityRestrictions(bidRequest, auctionContext); + return httpClient.post( + config.getIdentityResolutionEndpoint(), + headers(), + mapper.encodeToString(restrictedBidRequest), + config.getRequestTimeoutMs()) + .map(this::processResponse); + } + + private BidRequest applyActivityRestrictions(BidRequest bidRequest, AuctionContext auctionContext) { + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of( + ComponentType.GENERAL_MODULE, + LiveIntentOmniChannelIdentityModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); + final boolean disallowTransmitTid = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_TID, activityInvocationPayload); + + return maskUserPersonalInfo( + bidRequest, + disallowTransmitUfpd, + disallowTransmitEids, + disallowTransmitGeo, + disallowTransmitTid); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo, + boolean disallowTransmitTid) { + + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); + + final Source maskedSource = maskSource(bidRequest.getSource(), disallowTransmitUfpd, disallowTransmitTid); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .source(maskedSource) + .build(); + } + + private Source maskSource(Source source, boolean mastUfpd, boolean maskTid) { + if (source == null || !(mastUfpd || maskTid)) { + return source; + } + + return source.toBuilder().tid(null).build(); + } + + private MultiMap headers() { + return MultiMap.caseInsensitiveMultiMap() + .add(HttpUtil.AUTHORIZATION_HEADER, "Bearer " + config.getAuthToken()); + } + + private IdResResponse processResponse(HttpClientResponse response) { + return mapper.decodeValue(response.getBody(), IdResResponse.class); + } + + private static Future> noAction() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + private InvocationResultImpl update(IdResResponse resolutionResult) { + return InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> updatedPayload(payload, resolutionResult.getEids())) + .analyticsTags(TagsImpl.of(List.of( + ActivityImpl.of( + "liveintent-enriched", "success", + List.of( + ResultImpl.of( + "", + mapper.mapper().createObjectNode() + .put("treatmentRate", config.getTreatmentRate()), + null)))))) + .build(); + } + + private AuctionRequestPayload updatedPayload(AuctionRequestPayload requestPayload, List resolvedEids) { + final List eids = ListUtils.emptyIfNull(resolvedEids); + final BidRequest bidRequest = updateAllowedBidders(requestPayload.bidRequest(), resolvedEids); + final User updatedUser = Optional.ofNullable(bidRequest.getUser()) + .map(user -> user.toBuilder().eids(ListUtil.union(ListUtils.emptyIfNull(user.getEids()), eids))) + .orElseGet(() -> User.builder().eids(eids)) + .build(); + + return AuctionRequestPayloadImpl.of(bidRequest.toBuilder().user(updatedUser).build()); + } + + private BidRequest updateAllowedBidders(BidRequest bidRequest, List resolvedEids) { + if (targetBidders.isEmpty()) { + return bidRequest; + } + + final ExtRequest ext = bidRequest.getExt(); + final ExtRequestPrebid extPrebid = ext != null ? ext.getPrebid() : null; + final ExtRequestPrebidData extPrebidData = extPrebid != null ? extPrebid.getData() : null; + + final ExtRequestPrebid updatedExtPrebid = Optional.ofNullable(extPrebid) + .map(ExtRequestPrebid::toBuilder) + .orElseGet(ExtRequestPrebid::builder) + .data(updatePrebidData(extPrebidData, resolvedEids)) + .build(); + + final ExtRequest updatedExtRequest = ExtRequest.of(updatedExtPrebid); + if (ext != null) { + mapper.fillExtension(updatedExtRequest, ext.getProperties()); + } + + return bidRequest.toBuilder().ext(updatedExtRequest).build(); + } + + private ExtRequestPrebidData updatePrebidData(ExtRequestPrebidData extPrebidData, List resolvedEids) { + final List prebidDataBidders = extPrebidData != null ? extPrebidData.getBidders() : null; + final List updatedPrebidDataBidders = prebidDataBidders != null + ? (List) CollectionUtils.union(targetBidders, prebidDataBidders) + : targetBidders; + + final Set resolvedSources = resolvedEids.stream().map(Eid::getSource).collect(Collectors.toSet()); + + final List initialPermissions = Optional.ofNullable(extPrebidData) + .map(ExtRequestPrebidData::getEidPermissions) + .orElse(Collections.emptyList()); + final List updatedPermissions = Stream.concat( + initialPermissions.stream() + .map(permission -> updateEidPermission(permission, resolvedSources)), + resolvedSources.stream() + .map(source -> ExtRequestPrebidDataEidPermissions.of(source, targetBidders))) + .filter(StreamUtil.distinctBy(ExtRequestPrebidDataEidPermissions::getSource)) + .toList(); + + return ExtRequestPrebidData.of(updatedPrebidDataBidders, updatedPermissions); + } + + private ExtRequestPrebidDataEidPermissions updateEidPermission(ExtRequestPrebidDataEidPermissions permission, + Set resolvedSources) { + + return resolvedSources.contains(permission.getSource()) + ? ExtRequestPrebidDataEidPermissions.of( + permission.getSource(), + (List) CollectionUtils.union(permission.getBidders(), targetBidders)) + : permission; + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..7b05ec6072d --- /dev/null +++ b/extra/modules/live-intent-omni-channel-identity/src/test/java/org/prebid/server/hooks/modules/liveintent/omni/channel/identity/v1/LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest.java @@ -0,0 +1,471 @@ +package org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +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.activity.Activity; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.IdResResponse; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.model.config.LiveIntentOmniChannelProperties; +import org.prebid.server.hooks.modules.liveintent.omni.channel.identity.v1.hooks.LiveIntentOmniChannelIdentityProcessedAuctionRequestHook; +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.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; +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.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class LiveIntentOmniChannelIdentityProcessedAuctionRequestHookTest { + + private static final JacksonMapper MAPPER = new JacksonMapper(ObjectMapperProvider.mapper()); + + @Mock(strictness = LENIENT) + private UserFpdActivityMask userFpdActivityMask; + + @Mock + private HttpClient httpClient; + + @Mock(strictness = LENIENT) + private LiveIntentOmniChannelProperties properties; + + @Mock + private ActivityInfrastructure activityInfrastructure; + + @Mock + private AuctionInvocationContext auctionInvocationContext; + + @Mock + private AuctionContext auctionContext; + + private LiveIntentOmniChannelIdentityProcessedAuctionRequestHook target; + + private List configuredBidders; + + @BeforeEach + public void setUp() { + configuredBidders = List.of("bidder1", "bidder2"); + given(properties.getRequestTimeoutMs()).willReturn(5L); + given(properties.getIdentityResolutionEndpoint()).willReturn("https://test.com/idres"); + given(properties.getAuthToken()).willReturn("auth_token"); + given(properties.getTreatmentRate()).willReturn(1.0f); + given(properties.getTargetBidders()).willReturn(configuredBidders); + + target = new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + properties, userFpdActivityMask, MAPPER, httpClient, 0.01d); + } + + @Test + public void creationShouldFailOnInvalidIdentityUrl() { + given(properties.getIdentityResolutionEndpoint()).willReturn("invalid_url"); + assertThatIllegalArgumentException().isThrownBy(() -> + new LiveIntentOmniChannelIdentityProcessedAuctionRequestHook( + properties, userFpdActivityMask, MAPPER, httpClient, 0.01d)); + } + + @Test + public void geoPassingRestrictionShouldBeRespected() { + // given + final Geo givenGeo = Geo.builder() + .lat(52.51671856406936f) + .lon(13.377639726342583f) + .city("Berlin") + .country("Germany") + .build(); + final Device givenDevice = Device.builder() + .geo(givenGeo) + .ip("192.168.127.12") + .ifa("foo") + .macsha1("bar") + .macmd5("baz") + .dpidsha1("boo") + .dpidmd5("far") + .didsha1("zoo") + .didmd5("goo") + .build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").device(givenDevice).build(); + + final Geo expectedGeo = Geo.builder() + .lat(52.52f) + .lon(13.38f) + .build(); + final Device expectedDevice = Device.builder() + .geo(expectedGeo) + .ip("192.168.127.0") + .build(); + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().device(expectedDevice).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_GEO), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(true), eq(false))) + .will(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(true), eq(true))) + .will(invocation -> expectedDevice); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); + } + + @Test + public void tidPassingRestrictionShouldBeRespected() { + // given + final Source givenSource = Source.builder().tid("tid1").build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").source(givenSource).build(); + + final Source expectedSource = givenSource.toBuilder().tid(null).build(); + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().source(expectedSource).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_TID), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); + } + + @Test + public void eidPassingRestrictionShouldBeRespected() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final BidRequest expectedBidRequest = givenBidRequest.toBuilder().user(null).build(); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_EIDS), any())).willReturn(false); + given(activityInfrastructure.isAllowed(eq(Activity.TRANSMIT_UFPD), any())).willReturn(false); + given(userFpdActivityMask.maskUser(any(), eq(true), eq(true))) + .willReturn(null); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(expectedBidRequest)), + eq(5L)); + } + + @Test + public void callShouldEnrichUserEidsWithRequestedEids() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final Eid expectedEid = Eid.builder() + .source("liveintent.com") + .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getEids) + .isEqualTo(List.of(givenEid, expectedEid)); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } + + @Test + public void callShouldCreateUserAndUseRequestedEidsWhenUserIsAbsent() { + // given + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(null).build(); + + final Eid expectedEid = Eid.builder() + .source("liveintent.com") + .uids(singletonList(Uid.builder().id("id2").atype(3).build())) + .build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getEids) + .isEqualTo(List.of(expectedEid)); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } + + @Test + public void callShouldReturnNoActionSuccessfullyWhenTreatmentRateIsLowerThanThreshold() { + // given + final BidRequest givenBidRequest = BidRequest.builder().build(); + + given(properties.getTreatmentRate()).willReturn(0.0f); + + // when + final InvocationResult result = target.call( + AuctionRequestPayloadImpl.of(givenBidRequest), + auctionInvocationContext) + .result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + public void callShouldReturnFailureWhenRequestingEidsIsFailed() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givebEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givebEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.failedFuture(new TimeoutException("Timeout exceeded"))); + + // when + final Future> result = target.call( + AuctionRequestPayloadImpl.of(givenBidRequest), + auctionInvocationContext); + + // then + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(TimeoutException.class); + assertThat(result.cause()) + .isInstanceOf(TimeoutException.class) + .hasMessage("Timeout exceeded"); + } + + @Test + public void biddersConfiguredRestrictionShouldBeRespected() { + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).build(); + + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(configuredBidders, List.of( + ExtRequestPrebidDataEidPermissions.of("liveintent.com", configuredBidders))); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getData) + .isEqualTo(expectedData); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } + + @Test + public void biddersConfiguredRestrictionShouldBeMergedWithProvided() { + // given + final Uid givenUid = Uid.builder().id("id1").atype(2).build(); + final Eid givenEid = Eid.builder().source("some.source.com").uids(singletonList(givenUid)).build(); + final User givenUser = User.builder().eids(singletonList(givenEid)).build(); + final BidRequest givenBidRequest = BidRequest.builder().id("request").user(givenUser).ext(ExtRequest.of( + ExtRequestPrebid.builder().data(ExtRequestPrebidData.of(List.of("bidder3"), List.of( + ExtRequestPrebidDataEidPermissions.of("some.other-source.com", List.of("bidder3")), + ExtRequestPrebidDataEidPermissions.of("some.source.com", List.of("bidder3")))) + ).build())).build(); + + final List expectedBidders = List.of("bidder3", "bidder2", "bidder1"); + + final ExtRequestPrebidData expectedData = ExtRequestPrebidData.of(expectedBidders, List.of( + ExtRequestPrebidDataEidPermissions.of("some.other-source.com", List.of("bidder3")), + ExtRequestPrebidDataEidPermissions.of("some.source.com", List.of("bidder3")), + ExtRequestPrebidDataEidPermissions.of("liveintent.com", configuredBidders))); + + final Eid expectedEid = Eid.builder().source("liveintent.com").build(); + + final String responseBody = MAPPER.encodeToString(IdResResponse.of(List.of(expectedEid))); + given(httpClient.post(any(), any(), any(), anyLong())) + .willReturn(Future.succeededFuture(HttpClientResponse.of(200, null, responseBody))); + + given(auctionInvocationContext.auctionContext()).willReturn(auctionContext); + given(auctionContext.getActivityInfrastructure()).willReturn(activityInfrastructure); + given(activityInfrastructure.isAllowed(any(), any())).willReturn(true); + given(userFpdActivityMask.maskUser(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + given(userFpdActivityMask.maskDevice(any(), eq(false), eq(false))) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final InvocationResult result = + target.call(AuctionRequestPayloadImpl.of(givenBidRequest), auctionInvocationContext).result(); + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.payloadUpdate().apply(AuctionRequestPayloadImpl.of(givenBidRequest))) + .extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getData) + .isEqualTo(expectedData); + + verify(httpClient).post( + eq("https://test.com/idres"), + argThat(headers -> headers.contains("Authorization", "Bearer auth_token", true)), + eq(MAPPER.encodeToString(givenBidRequest)), + eq(5L)); + } +} diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md new file mode 100644 index 00000000000..3ae7bd5f659 --- /dev/null +++ b/extra/modules/optable-targeting/README.md @@ -0,0 +1,284 @@ +## Overview +Optable module operates using a DCN backend API. Please contact your account manager to get started. + +The optable-targeting module enriches an incoming OpenRTB request by adding to the `user.eids` and `user.data` +objects. Under the hood the module extracts PPIDs (publisher provided IDs) from the incoming request's `user.ext.eids`, +and also if present sha256-hashed email, sha256-hashed phone, zip or Optable Visitor ID provided correspondingly in the +`user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields (a full list of IDs is given in a table below). These IDs are +sent as input to the Targeting API. The received response data is used to enrich the OpenRTB request and response. +Targeting API endpoint is configurable per publisher. + +## Setup + +### Execution Plan + +This module runs at two stages: + +* Processed Auction Request: to enrich `user.eids` and `user.data`. +* Auction Response: to inject ad server targeting. + +We recommend defining the execution plan in the account config so the module is only invoked for specific accounts. See +below for an example. + +### Global Config + +There is no host-company level config for this module. + +### Account-Level Config + +To start using current module in PBS-Java you have to enable module and add +`optable-targeting-processed-auction-request-hook` and `optable-targeting-auction-response-hook` into hooks execution +plan inside your config file: +Here's a general template for the account config used in PBS-Java: + +```yaml +hooks: + optable-targeting: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 100, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } +``` + +Sample module enablement configuration in JSON and YAML formats: + +```json +{ + "modules": + { + "optable-targeting": + { + "api-endpoint": "endpoint", + "api-key": "key", + "timeout": 50, + "ppid-mapping": { + "pubcid.org": "c" + }, + "adserver-targeting": false + } + } +} +``` + +```yaml + modules: + optable-targeting: + api-endpoint: endpoint + api-key: key + timeout: 50 + ppid-mapping: { + "pubcid.org": "c" + } + adserver-targeting: false +``` + +### Timeout considerations + +The timeout value specified in the execution plan for the `processed-auction-request` hook is very important to be +picked such that the hook has enough time to make a roundtrip to Optable Targeting Edge API over HTTP. + +**Note:** Do not confuse hook timeout value with the module timeout parameter which is optional. The hook timeout value +would depend on the cloud/region where the PBS instance is hosted and the latency to reach the Optable's servers. This +will need to be verified experimentally upon deployment. + +The timeout value for the `auction-response` can be set to 10 ms - usually it will be sub-millisecond time as there are +no HTTP calls made in this hook - Optable-specific keywords are cached on the `processed-auction-request` stage and +retrieved from the module invocation context later. + +## Module Configuration Parameters for PBS-Java + +The parameter names are specified with full path using dot-notation. F.e. `section-name` .`sub-section` .`param-name` +would result in this nesting in the JSON configuration: + +```json +{ + "section-name": { + "sub-section": { + "param-name": "param-value" + } + } +} +``` + + +| Param Name | Required | Type | Default value | Description | +|:-------------------|:---------|:--------|:---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| api-endpoint | yes | string | none | Optable Targeting Edge API endpoint URL, required | +| api-key | no | string | none | If the API is protected with a key - this param needs to be specified to be sent in the auth header | +| ppid-mapping | no | map | none | This specifies PPID source (`user.ext.eids[].source`) to a custom identifier prefix mapping, f.e. `{"example.com" : "c"}`. See the section on ID Mapping below for more detail. | +| adserver-targeting | no | boolean | false | If set to true - will add the Optable-specific adserver targeting keywords into the PBS response for every `seatbid[].bid[].ext.prebid.targeting` | +| timeout | no | integer | false | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services | +| id-prefix-order | no | string | none | An optional string of comma separated id prefixes that prioritizes and specifies the order in which ids are provided to Targeting API in a query string. F.e. "c,c1,id5" will guarantee that Targeting API will see id=c:...,c1:...,id5:... if these ids are provided. id-prefixes not mentioned in this list will be added in arbitrary order after the priority prefix ids. This affects Targeting API processing logic | + +## ID Mapping + +Internally the module sends requests to Optable Targeting API. The output of Targeting API is used to enrich the request +and response. The below table describes the parameters that the module automatically fetches from OpenRTB request and +then sends to the Targeting API. The module will use a prefix as specified in the table to prepend the corresponding ID +value when sending it to the Targeting API in the form `id=prefix:value`. + +See [Optable documentation](https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes) +on identifier types. Targeting API accepts multiple id parameters - and their order may affect the results, thus +`id-prefix-order` specifies the order of the ids. + + +| Identifier Type | OpenRTB field | ID Type Prefix | +|--------------------------------------------------------------------------------|-----------------------------------------------------------------------|------------------------------------------| +| Email Address | `user.ext.optable.email` | `e:` | +| Phone Number | `user.ext.optable.phone` | `p:` | +| Postal Code | `user.ext.optable.zip` | `z:` | +| IPv4 Address | `device.ip` | ~~i4:~~ Sent as `X-Forwarded-For` header | +| IPv6 Address | `device.ipv6` | ~~i6:~~ Sent as `X-Forwarded-For` header | +| Apple IDFA | `device.ifa if lcase(device.os) contains 'ios' and device.lmt!=1` | `a:` | +| Google GAID | `device.ifa if lcase(device.os) contains 'android' and device.lmt!=1` | `g:` | +| Roku RIDA | `device.ifa if lcase(device.os) contains 'roku' and device.lmt!=1` | `r:` | +| Samsung TV TIFA | `device.ifa if lcase(device.os) contains 'tizen' and device.lmt!=1` | `s:` | +| Amazon Fire AFAI | `device.ifa if lcase(device.os) contains 'fire' and device.lmt!=1` | `f:` | +| [NetID](https://docs.prebid.org/dev-docs/modules/userid-submodules/netid.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="netid.de"` | `n:` | +| [ID5](https://docs.prebid.org/dev-docs/modules/userid-submodules/id5.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="id5-sync.com"` | `id5:` | +| [Utiq](https://docs.prebid.org/dev-docs/modules/userid-submodules/utiq.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="utiq.com"` | `utiq:` | +| Optable VID | `user.ext.optable.vid` | `v:` | + +### Optable input erasure + +**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields will be removed by the module from the original +OpenRTB request before being sent to bidders. + +### Publisher Provided IDs (PPID) Mapping + +Custom user IDs are sent in the OpenRTB request in the +[`user.ext.eids[]`](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#3227---object-eid-). +The `ppid-mapping` allows to specify the mapping of a source to one of the custom identifier type prefixes `c`-`c19` - +see [documentation](https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes), f.e.: + +```yaml +ppid-mapping: {"example.com": "c2", "test.com": "c3"} +``` + +It is also possible to override any of the automatically retrieved `user.ext.eids[]` mentioned in the table above (s.a. +id5, utiq) so they are mapped to a different prefix. f.e. `id5-sync.com` can be mapped to a prefix other than `id5:`, +like: + +```yaml +ppid-mapping: {"id5-sync.com": "c1"} +``` + +This will lead to id5 ID supplied as `id=c1:...` to the Targeting API. + +## Analytics Tags + +The following 2 analytics tags are written by the module: + +* `optable-enrich-request` +* `optable-enrich-response` + +The `status` is either `success` or `failure`. Where it is `failure` a `results[0].value.reason` is provided. +For the `optable-enrich-request` activity the `execution-time` value is logged. +Example: + +```json +{ + "analytics": { + "tags": [ + { + "stage": "auction-response", + "module": "optable-targeting", + "analyticstags": { + "activities": [ + { + "name": "optable-enrich-request", + "status": "success", + "results": [ + { + "values": { + "execution-time": 33 + } + } + ] + }, + { + "name": "optable-enrich-response", + "status": "success", + "results": [ + { + "values": { + "reason": "none" + } + } + ] + } + ] + } + } + ] + } +} +``` + +If `adserver-targeting` was set to `false` in the config `optable-enrich-response` analytics tag is not written. + +## Running the demo (PBS-Java) + +1. Build the server bundle JAR as described in [Build Project](https://github.com/prebid/prebid-server-java/blob/master/docs/build.md#build-project), e.g. + +```bash +mvn clean package --file extra/pom.xml +``` + +2. In the `sample/configs/prebid-config-optable.yaml` file specify the `api-endpoint` URL of your DCN, f.e.: + +```yaml +api-endpoint: https://example.com/v2/targeting +``` + +3. Start server bundle JAR as described in [Running project](https://github.com/prebid/prebid-server-java/blob/master/docs/run.md#running-project), e.g. + +```bash +java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/configs/prebid-config-with-optable.yaml +``` + +4. Run sample request against the server as described in [the sample directory](https://github.com/prebid/prebid-server-java/tree/master/sample), e.g. + +```bash +curl http://localhost:8080/openrtb2/auction --data @extra/modules/optable-targeting/sample-requests/data.json +``` + +5. Observe the `user.eids` and `user.data` objects enriched. + +## Maintainer contacts + +Any suggestions or questions can be directed to [prebid@optable.co](mailto:prebid@optable.co). + +Alternatively please open a 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. diff --git a/extra/modules/optable-targeting/lombok.config b/extra/modules/optable-targeting/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/optable-targeting/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml new file mode 100644 index 00000000000..9e6503995c0 --- /dev/null +++ b/extra/modules/optable-targeting/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + optable-targeting + + optable-targeting + Optable targeting module + diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json new file mode 100644 index 00000000000..d05f9a5eebc --- /dev/null +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -0,0 +1,127 @@ +{ + "test": 1, + "id": "1", + "imp": + [ + { + "id": "1", + "banner": + { + "w": 300, + "h": 250 + }, + "ext": + { + "prebid": + { + "storedauctionresponse": { "id": "optable-stored-response" }, + "bidder": + { + "appnexus": + { + "placementId": 0 + } + } + } + } + } + ], + "site": + { + "domain": "test.com", + "publisher": + { + "domain": "test.com", + "id": "1" + }, + "page": "https://www.test.com/" + }, + "device": + { + "ip": "8.8.8.8" + }, + "user": + { + "ext": + { + "optable": + { + "email": "5837d278eabede28e37b5766399ed0d1a4cdc36acee8d35710a255032f45beda" + }, + "eids": + [ + { + "source": "growthcode.io", + "uids": + [ + { + "id": "fb58593e-7ac6-48bd-b2de-89a758726362", + "atype": 1 + } + ] + }, + { + "source": "pubcid.org", + "uids": [ + { + "id": "test", + "atype": 1 + } + ] + }, + { + "source": "crwdcntrl.net", + "uids": + [ + { + "id": "dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00", + "atype": 1 + } + ] + }, + { + "source": "amxdt.net", + "uids": + [ + { + "id": "amx*3*a583802a-e6fe-48d7-87c6-7db1b6a4a73a*70f06cdcf8ab0b4ac07a56860ed0e0b6ef0388dc0b0ab5a1dd725999d3b339cf", + "atype": 1 + } + ] + }, + { + "source": "audigent.com", + "uids": + [ + { + "id": "f84456cd3c72296d7898f62e1c46dd964206ff4d47e64b690c3c5a1d6b1bd286", + "atype": 1 + } + ] + }, + { + "source": "adnxs.com", + "uids": + [ + { + "id": "d4fd63f0f4f7ce0d128348cb145c7e0f" + } + ] + } + ] + } + }, + "ext": { + "prebid": { + "targeting": { + "includebidderkeys": true + }, + "analytics": + { + "options": { + "enableclientdetails": true + } + } + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java new file mode 100644 index 00000000000..91afba88a31 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -0,0 +1,107 @@ +package org.prebid.server.hooks.modules.optable.targeting.config; + +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.json.ObjectMapperProvider; +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; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + OptableTargetingModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class OptableTargetingConfig { + + @Bean + @ConfigurationProperties(prefix = "hooks.modules." + OptableTargetingModule.CODE) + OptableTargetingProperties optableTargetingProperties() { + return new OptableTargetingProperties(); + } + + @Bean + IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate); + } + + @Bean + APIClientImpl apiClient(HttpClient httpClient, + @Value("${logging.sampling-rate:0.01}") + double logSamplingRate, + OptableTargetingProperties optableTargetingProperties, + JacksonMapper jacksonMapperr) { + + return new APIClientImpl( + optableTargetingProperties.getApiEndpoint(), + httpClient, + jacksonMapperr, + logSamplingRate); + } + + @Bean + @ConditionalOnProperty(name = {"storage.pbc.enabled", "cache.module.enabled"}, havingValue = "true") + CachedAPIClient cachedApiClient(APIClientImpl apiClient, + Cache cache, + @Value("${http-client.circuit-breaker.enabled:false}") + boolean isCircuitBreakerEnabled) { + + return new CachedAPIClient(apiClient, cache, isCircuitBreakerEnabled); + } + + @Bean + @ConditionalOnProperty(name = {"storage.pbc.enabled", "cache.module.enabled"}, havingValue = "true") + Cache cache(PbcStorageService cacheService, JacksonMapper jacksonMapper) { + return new Cache(cacheService, jacksonMapper); + } + + @Bean + OptableTargeting optableTargeting(IdsMapper parametersExtractor, + APIClientImpl apiClient, + @Autowired(required = false) CachedAPIClient cachedApiClient) { + + return new OptableTargeting( + parametersExtractor, + ObjectUtils.firstNonNull(cachedApiClient, apiClient)); + } + + @Bean + ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties globalProperties) { + return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); + } + + @Bean + OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, + OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + JsonMerger jsonMerger, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new OptableTargetingModule(List.of( + new OptableTargetingProcessedAuctionRequestHook( + configResolver, + optableTargeting, + userFpdActivityMask, + logSamplingRate), + new OptableTargetingAuctionResponseHook( + configResolver, + ObjectMapperProvider.mapper(), + jsonMerger))); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java new file mode 100644 index 00000000000..e0dc94d34cf --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class EnrichmentStatus { + + Status status; + + Reason reason; + + public static EnrichmentStatus failure() { + return EnrichmentStatus.of(Status.FAIL, null); + } + + public static EnrichmentStatus success() { + return EnrichmentStatus.of(Status.SUCCESS, null); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java new file mode 100644 index 00000000000..d80057fddde --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java @@ -0,0 +1,42 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +import javax.validation.constraints.NotNull; + +@Value(staticConstructor = "of") +public class Id { + + public static final String EMAIL = "e"; + + public static final String PHONE = "p"; + + public static final String ZIP = "z"; + + public static final String DEVICE_IP_V_4 = "i4"; + + public static final String DEVICE_IP_V_6 = "i6"; + + public static final String APPLE_IDFA = "a"; + + public static final String GOOGLE_GAID = "g"; + + public static final String ROKU_RIDA = "r"; + + public static final String SAMSUNG_TV_TIFA = "s"; + + public static final String AMAZON_FIRE_AFAI = "f"; + + public static final String NET_ID = "n"; + + public static final String ID5 = "id5"; + + public static final String UTIQ = "utiq"; + + public static final String OPTABLE_VID = "v"; + + @NotNull + String name; + + String value; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java new file mode 100644 index 00000000000..ed0264f0249 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Data; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +import java.util.List; + +@Data +public class ModuleContext { + + private List targeting; + + private EnrichmentStatus enrichRequestStatus; + + private EnrichmentStatus enrichResponseStatus; + + private boolean adserverTargetingEnabled; + + private long optableTargetingExecutionTime; + + public static ModuleContext of(AuctionInvocationContext invocationContext) { + final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); + return moduleContext != null ? moduleContext : new ModuleContext(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java new file mode 100644 index 00000000000..8af2273115e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +public enum OS { + + IOS("ios"), + + ANDROID("android"), + + ROKU("roku"), + + TIZEN("tizen"), + + FIRE("fire"); + + private final String value; + + OS(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java new file mode 100644 index 00000000000..9dacd6de322 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Set; + +@Value +@Builder(toBuilder = true) +public class OptableAttributes { + + String gpp; + + Set gppSid; + + String gdprConsent; + + boolean gdprApplies; + + List ips; + + String userAgent; + + Long timeout; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java new file mode 100644 index 00000000000..20862050f39 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Query { + + String ids; + + String attributes; + + public String toQueryString() { + return ids + attributes; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java new file mode 100644 index 00000000000..b5b13f69364 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Getter; + +public enum Reason { + + NONE("none"), + NOBID("nobid"), + NOKEYWORD("nokeyword"); + + @Getter + private final String value; + + Reason(String value) { + this.value = value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java new file mode 100644 index 00000000000..dcc03428a7a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Getter; + +public enum Status { + + SUCCESS("success"), + FAIL("fail"); + + @Getter + private final String value; + + Status(String value) { + this.value = value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java new file mode 100644 index 00000000000..555c5b01277 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.config; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class CacheProperties { + + private boolean enabled = false; + + private int ttlseconds = 86400; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java new file mode 100644 index 00000000000..7f0598da83e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -0,0 +1,45 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; +import java.util.Set; + +@Data +@NoArgsConstructor +public final class OptableTargetingProperties { + + @JsonProperty("api-endpoint") + String apiEndpoint; + + @JsonProperty("api-key") + String apiKey; + + String tenant; + + String origin; + + @JsonProperty("ppid-mapping") + Map ppidMapping; + + @JsonProperty("adserver-targeting") + Boolean adserverTargeting = true; + + Long timeout; + + @JsonProperty("id-prefix-order") + String idPrefixOrder; + + @JsonProperty("optable-inserter-eids-merge") + Set optableInserterEidsMerge = Set.of(); + + @JsonProperty("optable-inserter-eids-replace") + Set optableInserterEidsReplace = Set.of(); + + @JsonProperty("optable-inserter-eids-ignore") + Set optableInserterEidsIgnore = Set.of(); + + CacheProperties cache = new CacheProperties(); +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java new file mode 100644 index 00000000000..566c46c2c35 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +import java.util.List; + +@Value +public class Audience { + + String provider; + + List ids; + + String keyspace; + + Integer rtbSegtax; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java new file mode 100644 index 00000000000..73d7953fbdc --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +@Value +public class AudienceId { + + String id; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java new file mode 100644 index 00000000000..787b3adbf1a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java @@ -0,0 +1,20 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.FlexibleExtension; + +@EqualsAndHashCode(callSuper = true) +@Builder(toBuilder = true) +@Value +public class ExtUserOptable extends FlexibleExtension { + + String email; + + String phone; + + String zip; + + String vid; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java new file mode 100644 index 00000000000..f8dace457af --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +@Value +public class Ortb2 { + + User user; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java new file mode 100644 index 00000000000..826ce2698ed --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +import java.util.List; + +@Value +public class TargetingResult { + + List audience; + + Ortb2 ortb2; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java new file mode 100644 index 00000000000..1ad2cdb220b --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import lombok.Value; + +import java.util.List; + +@Value +public class User { + + List eids; + + List data; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java new file mode 100644 index 00000000000..5a20f79a347 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -0,0 +1,104 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +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.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; + +import java.util.List; +import java.util.Objects; + +public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { + + private static final String CODE = "optable-targeting-auction-response-hook"; + + private final ConfigResolver configResolver; + private final ObjectMapper objectMapper; + private final JsonMerger jsonMerger; + + public OptableTargetingAuctionResponseHook(ConfigResolver configResolver, + ObjectMapper objectMapper, + JsonMerger jsonMerger) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.objectMapper = Objects.requireNonNull(objectMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + @Override + public Future> call(AuctionResponsePayload auctionResponsePayload, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final boolean adserverTargeting = properties.getAdserverTargeting(); + + final ModuleContext moduleContext = ModuleContext.of(invocationContext); + moduleContext.setAdserverTargetingEnabled(adserverTargeting); + + if (!adserverTargeting) { + return success(moduleContext); + } + + final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( + auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); + moduleContext.setEnrichResponseStatus(validationStatus); + + return validationStatus.getStatus() == Status.SUCCESS + ? enrichedPayload(moduleContext) + : success(moduleContext); + } + + private Future> enrichedPayload(ModuleContext moduleContext) { + final List targeting = moduleContext.getTargeting(); + + return CollectionUtils.isNotEmpty(targeting) + ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext) + : success(moduleContext); + } + + private Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) + .build()); + } + + private Future> success(ModuleContext moduleContext) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(moduleContext) + .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java new file mode 100644 index 00000000000..0b36b276b57 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.optable.targeting.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 class OptableTargetingModule implements Module { + + public static final String CODE = "optable-targeting"; + + private final Collection> hooks; + + public OptableTargetingModule(Collection> hooks) { + this.hooks = hooks; + } + + @Override + public Collection> hooks() { + return hooks; + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..a5ad2559d40 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -0,0 +1,176 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +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.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +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.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; + +public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); + + public static final String CODE = "optable-targeting-processed-auction-request-hook"; + + private final ConfigResolver configResolver; + private final OptableTargeting optableTargeting; + private final UserFpdActivityMask userFpdActivityMask; + private final double logSamplingRate; + + public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, + OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + double logSamplingRate) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.optableTargeting = Objects.requireNonNull(optableTargeting); + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.logSamplingRate = logSamplingRate; + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final ModuleContext moduleContext = new ModuleContext(); + final long callTargetingAPITimestamp = System.currentTimeMillis(); + + if (!isTargetingPropertiesValid(properties)) { + conditionalLogger.error( + "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + + moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return update(BidRequestCleaner.instance(), moduleContext); + } + + final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); + + final Timeout timeout = getHookTimeout(invocationContext); + final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( + invocationContext.auctionContext(), + properties.getTimeout()); + + return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) + .compose(targetingResult -> { + moduleContext.setOptableTargetingExecutionTime( + System.currentTimeMillis() - callTargetingAPITimestamp); + return enrichedPayload(targetingResult, moduleContext, properties); + }) + .recover(throwable -> { + moduleContext.setOptableTargetingExecutionTime( + System.currentTimeMillis() - callTargetingAPITimestamp); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return update(BidRequestCleaner.instance(), moduleContext); + }); + } + + private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { + return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); + } + + private BidRequest applyActivityRestrictions(BidRequest bidRequest, + AuctionInvocationContext auctionInvocationContext) { + + final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); + + return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo) { + + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .build(); + } + + private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { + return invocationContext.timeout(); + } + + private Future> enrichedPayload(TargetingResult targetingResult, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + return update( + BidRequestCleaner.instance() + .andThen(BidRequestEnricher.of(targetingResult, properties)) + ::apply, + moduleContext); + } + + private static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java new file mode 100644 index 00000000000..80905ce088c --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java @@ -0,0 +1,68 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class AnalyticTagsResolver { + + private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; + private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; + private static final String STATUS_EXECUTION_TIME = "execution-time"; + private static final String STATUS_REASON = "reason"; + + private AnalyticTagsResolver() { + } + + public static Tags toEnrichRequestAnalyticTags(ModuleContext moduleContext) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY_ENRICH_REQUEST, + toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()), + toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime()))))); + } + + public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) { + final List activities = new ArrayList<>(); + if (moduleContext.isAdserverTargetingEnabled()) { + activities.add(ActivityImpl.of( + ACTIVITY_ENRICH_RESPONSE, + toEnrichmentStatusValue(moduleContext.getEnrichResponseStatus()), + toResults(STATUS_REASON, toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus())))); + } + + return TagsImpl.of(Collections.unmodifiableList(activities)); + } + + private static String toEnrichmentStatusValue(EnrichmentStatus enrichRequestStatus) { + return Optional.ofNullable(enrichRequestStatus) + .map(EnrichmentStatus::getStatus) + .map(Status::getValue) + .orElse(null); + } + + private static String toEnrichmentStatusReason(EnrichmentStatus enrichmentStatus) { + return Optional.ofNullable(enrichmentStatus) + .map(EnrichmentStatus::getReason) + .map(Reason::getValue) + .orElse(null); + } + + private static List toResults(String result, String value) { + final ObjectNode resultDetails = ObjectMapperProvider.mapper().createObjectNode().put(result, value); + return Collections.singletonList(ResultImpl.of(null, resultDetails, null)); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java new file mode 100644 index 00000000000..44b22f8b92e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java @@ -0,0 +1,50 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class AuctionResponseValidator { + + private AuctionResponseValidator() { + } + + public static EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List targeting) { + if (!hasKeywords(targeting)) { + return EnrichmentStatus.of(Status.FAIL, Reason.NOKEYWORD); + } else if (!hasBids(bidResponse)) { + return EnrichmentStatus.of(Status.FAIL, Reason.NOBID); + } + + return EnrichmentStatus.of(Status.SUCCESS, Reason.NONE); + } + + private static boolean hasKeywords(List targeting) { + if (CollectionUtils.isEmpty(targeting)) { + return false; + } + + return targeting.stream() + .filter(Objects::nonNull) + .anyMatch(audience -> CollectionUtils.isNotEmpty(audience.getIds())); + } + + private static boolean hasBids(BidResponse bidResponse) { + final List seatBids = Optional.ofNullable(bidResponse).map(BidResponse::getSeatbid).orElse(null); + if (CollectionUtils.isEmpty(seatBids)) { + return false; + } + + return seatBids.stream() + .filter(Objects::nonNull) + .anyMatch(seatBid -> CollectionUtils.isNotEmpty(seatBid.getBid())); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java new file mode 100644 index 00000000000..30652383942 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java @@ -0,0 +1,49 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.List; + +public class BidRequestCleaner implements PayloadUpdate { + + private static final String OPTABLE_FIELD = "optable"; + private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid"); + + public static BidRequestCleaner instance() { + return new BidRequestCleaner(); + } + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); + } + + private static BidRequest clearExtUserOptable(BidRequest bidRequest) { + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + final JsonNode optable = extUser != null ? extUser.getProperty(OPTABLE_FIELD) : null; + if (optable == null || !optable.isObject() || optable.isEmpty()) { + return bidRequest; + } + + final ObjectNode cleanedOptable = cleanOptable((ObjectNode) optable); + if (cleanedOptable.isEmpty()) { + extUser.addProperty(OPTABLE_FIELD, null); + } else { + extUser.addProperty(OPTABLE_FIELD, cleanedOptable); + } + + return bidRequest; + } + + public static ObjectNode cleanOptable(ObjectNode optable) { + return optable.deepCopy().remove(FIELDS_TO_REMOVE); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java new file mode 100644 index 00000000000..2a60389802c --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -0,0 +1,203 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidRequestEnricher implements PayloadUpdate { + + private static final String OPTABLE_CO_INSERTER = "optable.co"; + + private final TargetingResult targetingResult; + private final OptableTargetingProperties targetingProperties; + + private BidRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) { + this.targetingResult = targetingResult; + this.targetingProperties = targetingProperties; + } + + public static BidRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) { + return new BidRequestEnricher(targetingResult, properties); + } + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest())); + } + + private BidRequest enrichBidRequest(BidRequest bidRequest) { + if (bidRequest == null || targetingResult == null) { + return bidRequest; + } + + final User optableUser = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .orElse(null); + + if (optableUser == null) { + return bidRequest; + } + + final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) + .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); + + return bidRequest.toBuilder() + .user(mergeUserData(bidRequestUser, optableUser)) + .build(); + } + + private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + return user.toBuilder() + .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids()))) + .data(mergeData(user.getData(), optableUser.getData())) + .build(); + } + + private List mergeEids(List destination, List source) { + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToSourceEid = source.stream().collect(Collectors.toMap( + BidRequestEnricher::eidIdExtractor, + Function.identity(), + (a, b) -> b, + HashMap::new)); + + final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); + final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge() + .stream() + .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet()); + + final List mergedEid = destination.stream() + .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid)) + && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter()) + ? resolveEidConflict( + destinationEid, + idToSourceEid.get(eidIdExtractor(destinationEid)), + sourceToMerge, + sourceToReplace) + : destinationEid) + .toList(); + + return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor); + } + + private List filterOptableEids(List eids) { + if (CollectionUtils.isEmpty(eids)) { + return eids; + } + + final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore(); + if (CollectionUtils.isEmpty(optableIdsToIgnore)) { + return eids; + } + + return eids.stream() + .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter()) + || !optableIdsToIgnore.contains(eid.getSource())) + .toList(); + } + + private static Eid resolveEidConflict(Eid destinationEid, + Eid sourceEid, + Set sourceToMerge, + Set sourceToReplace) { + + final String eidSource = sourceEid.getSource(); + + if (sourceToReplace.contains(eidSource)) { + return sourceEid; + } + if (sourceToMerge.contains(eidSource)) { + return mergeEid(destinationEid, sourceEid); + } + + return destinationEid; + } + + private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { + return destinationEid.toBuilder() + .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId)) + .build(); + } + + private static String eidIdExtractor(Eid eid) { + return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource()); + } + + private static List mergeData(List destination, List source) { + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToSourceData = source.stream() + .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new)); + + final List mergedData = destination.stream() + .map(destinationData -> idToSourceData.containsKey(destinationData.getId()) + ? mergeData(destinationData, idToSourceData.get(destinationData.getId())) + : destinationData) + .toList(); + + return merge(mergedData, source, Data::getId); + } + + private static Data mergeData(Data destinationData, Data sourceData) { + return destinationData.toBuilder() + .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId)) + .build(); + } + + private static List merge(List destination, + List source, + Function idExtractor) { + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + final Set existingIds = destination.stream() + .map(idExtractor) + .collect(Collectors.toSet()); + + return Stream.concat( + destination.stream(), + source.stream() + .filter(entry -> !existingIds.contains(idExtractor.apply(entry)))) + .toList(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java new file mode 100644 index 00000000000..ec9fe4ef0a7 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java @@ -0,0 +1,110 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +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.exception.InvalidRequestException; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class BidResponseEnricher implements PayloadUpdate { + + private final List targeting; + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + + private BidResponseEnricher(List targeting, ObjectMapper mapper, JsonMerger jsonMerger) { + this.targeting = targeting; + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public static BidResponseEnricher of(List targeting, ObjectMapper mapper, JsonMerger jsonMerger) { + return new BidResponseEnricher(targeting, mapper, jsonMerger); + } + + @Override + public AuctionResponsePayload apply(AuctionResponsePayload payload) { + return AuctionResponsePayloadImpl.of(enrichBidResponse(payload.bidResponse(), targeting)); + } + + private BidResponse enrichBidResponse(BidResponse bidResponse, List targeting) { + if (CollectionUtils.isEmpty(targeting)) { + return bidResponse; + } + + final ObjectNode node = targetingToObjectNode(targeting); + if (node.isEmpty()) { + return bidResponse; + } + + final List seatBids = CollectionUtils.emptyIfNull(bidResponse.getSeatbid()).stream() + .map(seatBid -> seatBid.toBuilder() + .bid(CollectionUtils.emptyIfNull(seatBid.getBid()).stream() + .map(bid -> applyTargeting(bid, node)) + .toList()) + .build()) + .toList(); + + return bidResponse.toBuilder() + .seatbid(seatBids) + .build(); + } + + private ObjectNode targetingToObjectNode(List targeting) { + final ObjectNode node = mapper.createObjectNode(); + + for (Audience audience : targeting) { + final List ids = audience.getIds(); + if (CollectionUtils.isEmpty(ids)) { + continue; + } + + final String joinedIds = ids.stream() + .map(AudienceId::getId) + .collect(Collectors.joining(",")); + node.putIfAbsent(audience.getKeyspace(), TextNode.valueOf(joinedIds)); + } + + return node; + } + + private Bid applyTargeting(Bid bid, ObjectNode node) { + final ObjectNode ext = Optional.ofNullable(bid.getExt()) + .map(ObjectNode::deepCopy) + .orElseGet(mapper::createObjectNode); + + final ObjectNode prebid = newNodeIfNull(ext.get("prebid")); + final ObjectNode targeting; + try { + targeting = (ObjectNode) jsonMerger.merge(node, newNodeIfNull(prebid.get("targeting"))); + } catch (InvalidRequestException e) { + return bid; + } + + prebid.set("targeting", targeting); + ext.set("prebid", prebid); + + return bid.toBuilder().ext(ext).build(); + } + + private ObjectNode newNodeIfNull(JsonNode node) { + return node == null || !node.isObject() + ? mapper.createObjectNode() + : (ObjectNode) node; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java new file mode 100644 index 00000000000..97fdddf4a89 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -0,0 +1,44 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import io.vertx.core.Future; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; + +import java.util.Objects; + +public class Cache { + + private static final String APPLICATION = "prebid-Java"; + private static final String APP_CODE = "optable-targeting"; + + private final PbcStorageService cacheService; + private final JacksonMapper mapper; + + public Cache(PbcStorageService cacheService, JacksonMapper mapper) { + this.cacheService = Objects.requireNonNull(cacheService); + this.mapper = Objects.requireNonNull(mapper); + } + + public Future get(String query) { + return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) + .map(ModuleCacheResponse::getValue) + .map(body -> body != null ? mapper.decodeValue(body, TargetingResult.class) : null); + } + + public Future put(String query, TargetingResult value, int ttlSeconds) { + if (value == null) { + return Future.succeededFuture(); + } + + return cacheService.storeEntry( + query, + mapper.encodeToString(value), + StorageDataType.TEXT, + ttlSeconds, + APPLICATION, + APP_CODE); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java new file mode 100644 index 00000000000..219f37af2ab --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.json.JsonMerger; + +import java.util.Objects; +import java.util.Optional; + +public class ConfigResolver { + + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + private final OptableTargetingProperties globalProperties; + private final JsonNode globalPropertiesObjectNode; + + public ConfigResolver(ObjectMapper mapper, JsonMerger jsonMerger, OptableTargetingProperties globalProperties) { + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.globalProperties = Objects.requireNonNull(globalProperties); + this.globalPropertiesObjectNode = Objects.requireNonNull(mapper.valueToTree(globalProperties)); + } + + public OptableTargetingProperties resolve(ObjectNode configNode) { + final JsonNode mergedNode = jsonMerger.merge(configNode, globalPropertiesObjectNode); + return parse(mergedNode).orElse(globalProperties); + } + + private Optional parse(JsonNode configNode) { + try { + return Optional.ofNullable(configNode) + .filter(node -> !node.isEmpty()) + .map(node -> mapper.convertValue(node, OptableTargetingProperties.class)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java new file mode 100644 index 00000000000..7214f6ce6a3 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -0,0 +1,133 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OS; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class IdsMapper { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class)); + + private static final Map STATIC_PPID_MAPPING = Map.of( + "id5-sync.com", Id.ID5, + "utiq.com", Id.UTIQ, + "netid.de", Id.NET_ID); + + private final ObjectMapper objectMapper; + private final double logSamplingRate; + + public IdsMapper(ObjectMapper objectMapper, double logSamplingRate) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.logSamplingRate = logSamplingRate; + } + + public List toIds(BidRequest bidRequest, Map ppidMapping) { + final User user = bidRequest.getUser(); + + final Map ids = new HashMap<>(); + addOptableIds(ids, user); + addDeviceIds(ids, bidRequest.getDevice()); + addEidsIds(ids, user, STATIC_PPID_MAPPING); + addEidsIds(ids, user, ppidMapping); + + return ids.entrySet().stream() + .map(it -> Id.of(it.getKey(), it.getValue())) + .toList(); + } + + private void addOptableIds(Map ids, User user) { + final Optional extUserOptable = Optional.ofNullable(user) + .map(User::getExt) + .map(ext -> ext.getProperty("optable")) + .map(this::parseExtUserOptable); + + extUserOptable.map(ExtUserOptable::getEmail).ifPresent(it -> ids.put(Id.EMAIL, it)); + extUserOptable.map(ExtUserOptable::getPhone).ifPresent(it -> ids.put(Id.PHONE, it)); + extUserOptable.map(ExtUserOptable::getZip).ifPresent(it -> ids.put(Id.ZIP, it)); + extUserOptable.map(ExtUserOptable::getVid).ifPresent(it -> ids.put(Id.OPTABLE_VID, it)); + } + + private ExtUserOptable parseExtUserOptable(JsonNode node) { + try { + return objectMapper.treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); + return null; + } + } + + private static void addDeviceIds(Map ids, Device device) { + final String ifa = device != null ? device.getIfa() : null; + final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; + final int lmt = Optional.ofNullable(device).map(Device::getLmt).orElse(0); + + if (ifa == null || StringUtils.isEmpty(os) || lmt == 1) { + return; + } + + if (os.contains(OS.IOS.getValue())) { + ids.put(Id.APPLE_IDFA, ifa); + } + if (os.contains(OS.ANDROID.getValue())) { + ids.put(Id.GOOGLE_GAID, ifa); + } + if (os.contains(OS.ROKU.getValue())) { + ids.put(Id.ROKU_RIDA, ifa); + } + if (os.contains(OS.TIZEN.getValue())) { + ids.put(Id.SAMSUNG_TV_TIFA, ifa); + } + if (os.contains(OS.FIRE.getValue())) { + ids.put(Id.AMAZON_FIRE_AFAI, ifa); + } + } + + private static void addEidsIds(Map ids, User user, Map ppidMapping) { + final List eids = user != null ? user.getEids() : null; + if (MapUtils.isEmpty(ppidMapping) || CollectionUtils.isEmpty(eids)) { + return; + } + + for (Eid eid : eids) { + final String source = eid != null ? eid.getSource() : null; + if (source == null) { + continue; + } + + final String idKey = ppidMapping.get(source); + if (idKey != null) { + firstUidId(eid).ifPresent(it -> ids.put(idKey, it)); + } + } + } + + private static Optional firstUidId(Eid eid) { + return Optional.ofNullable(eid.getUids()) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(Uid::getId); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java new file mode 100644 index 00000000000..7f2aad0657d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.Device; +import org.apache.commons.collections4.SetUtils; +import org.prebid.server.auction.gpp.model.GppContext; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.privacy.gdpr.model.TcfContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class OptableAttributesResolver { + + private OptableAttributesResolver() { + } + + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { + final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); + final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); + + final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() + .ips(resolveIp(auctionContext)) + .userAgent(resolveUserAgent(auctionContext)) + .timeout(timeout); + + if (tcfContext.isConsentValid()) { + builder + .gdprApplies(tcfContext.isInGdprScope()) + .gdprConsent(tcfContext.getConsentString()); + } + + if (gppScope.getGppModel() != null) { + builder + .gpp(gppScope.getGppModel().encode()) + .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); + } + + return builder.build(); + } + + public static String resolveUserAgent(AuctionContext auctionContext) { + final Device device = auctionContext.getBidRequest().getDevice(); + return device != null ? device.getUa() : null; + } + + private static List resolveIp(AuctionContext auctionContext) { + final List result = new ArrayList<>(); + + final Optional deviceOpt = Optional.ofNullable(auctionContext.getBidRequest().getDevice()); + deviceOpt.map(Device::getIp).ifPresent(result::add); + deviceOpt.map(Device::getIpv6).ifPresent(result::add); + + if (result.isEmpty()) { + Optional.ofNullable(auctionContext.getPrivacyContext().getIpAddress()) + .ifPresent(result::add); + } + + return result; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java new file mode 100644 index 00000000000..0cb16d9c456 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -0,0 +1,39 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; + +import java.util.List; +import java.util.Objects; + +public class OptableTargeting { + + private final IdsMapper idsMapper; + private final APIClient apiClient; + + public OptableTargeting(IdsMapper idsMapper, APIClient apiClient) { + this.idsMapper = Objects.requireNonNull(idsMapper); + this.apiClient = Objects.requireNonNull(apiClient); + } + + public Future getTargeting(OptableTargetingProperties properties, + BidRequest bidRequest, + OptableAttributes attributes, + Timeout timeout) { + + final List ids = idsMapper.toIds(bidRequest, properties.getPpidMapping()); + final Query query = QueryBuilder.build(ids, attributes, properties.getIdPrefixOrder()); + if (query == null) { + return Future.failedFuture("Can't get targeting"); + } + + return apiClient.getTargeting(properties, query, attributes.getIps(), attributes.getUserAgent(), timeout); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java new file mode 100644 index 00000000000..613286f4b92 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -0,0 +1,90 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class QueryBuilder { + + private static final String REQUEST_SOURCE = "prebid-server"; + + private QueryBuilder() { + } + + public static Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { + if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) { + return null; + } + + return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes)); + } + + private static String buildIdsString(List ids, String idPrefixOrder) { + if (CollectionUtils.isEmpty(ids)) { + return StringUtils.EMPTY; + } + + final List reorderedIds = reorderIds(ids, idPrefixOrder); + + final StringBuilder sb = new StringBuilder(); + for (Id id : reorderedIds) { + sb.append("&id="); + sb.append(URLEncoder.encode( + "%s:%s".formatted(id.getName(), id.getValue()), + StandardCharsets.UTF_8)); + } + + return sb.toString(); + } + + private static List reorderIds(List ids, String idPrefixOrder) { + if (StringUtils.isEmpty(idPrefixOrder)) { + return ids; + } + + final String[] prefixOrder = idPrefixOrder.split(","); + final Map prefixToPriority = IntStream.range(0, prefixOrder.length).boxed() + .collect(Collectors.toMap(i -> prefixOrder[i], Function.identity())); + + final List orderedIds = new ArrayList<>(ids); + orderedIds.sort(Comparator.comparing(item -> prefixToPriority.getOrDefault(item.getName(), Integer.MAX_VALUE))); + + return orderedIds; + } + + private static String buildAttributesString(OptableAttributes optableAttributes) { + final StringBuilder sb = new StringBuilder(); + + Optional.ofNullable(optableAttributes.getGdprConsent()) + .ifPresent(consent -> sb.append("&gdpr_consent=").append(consent)); + sb.append("&gdpr=").append(optableAttributes.isGdprApplies() ? 1 : 0); + + Optional.ofNullable(optableAttributes.getGpp()) + .ifPresent(gpp -> sb.append("&gpp=").append(gpp)); + Optional.ofNullable(optableAttributes.getGppSid()) + .filter(Predicate.not(Collection::isEmpty)) + .ifPresent(gppSids -> sb.append("&gpp_sid=").append(gppSids.stream().findFirst())); + + Optional.ofNullable(optableAttributes.getTimeout()) + .ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); + + sb.append("&osdk=").append(REQUEST_SOURCE); + + return sb.toString(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java new file mode 100644 index 00000000000..ecb96d39cda --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; + +import java.util.List; + +public interface APIClient { + + Future getTargeting(OptableTargetingProperties properties, + Query query, + List ips, + String userAgent, + Timeout timeout); +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java new file mode 100644 index 00000000000..d31929f111f --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java @@ -0,0 +1,113 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +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.util.HttpUtil; +import org.prebid.server.validation.ValidationException; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.Objects; + +public class APIClientImpl implements APIClient { + + private static final Logger logger = LoggerFactory.getLogger(APIClientImpl.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private static final String TENANT = "{{TENANT}}"; + private static final String ORIGIN = "{{ORIGIN}}"; + + private final String endpoint; + private final HttpClient httpClient; + private final JacksonMapper mapper; + private final double logSamplingRate; + + public APIClientImpl(String endpoint, + HttpClient httpClient, + JacksonMapper mapper, + double logSamplingRate) { + + this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.httpClient = Objects.requireNonNull(httpClient); + this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; + } + + public Future getTargeting(OptableTargetingProperties properties, + Query query, + List ips, + String userAgent, + Timeout timeout) { + + final String uri = resolveEndpoint(properties.getTenant(), properties.getOrigin()); + final String queryAsString = query.toQueryString(); + final MultiMap headers = headers(properties, ips, userAgent); + + return httpClient.get(uri + queryAsString, headers, timeout.remaining()) + .compose(this::validateResponse) + .map(this::parseResponse) + .onFailure(exception -> logError(exception, uri)); + } + + private String resolveEndpoint(String tenant, String origin) { + return endpoint + .replace(TENANT, tenant) + .replace(ORIGIN, origin); + } + + private static MultiMap headers(OptableTargetingProperties properties, List ips, String userAgent) { + final MultiMap headers = HeadersMultiMap.headers() + .add(HttpUtil.ACCEPT_HEADER, "application/json"); + + if (userAgent != null) { + headers.add(HttpUtil.USER_AGENT_HEADER, userAgent); + } + final String apiKey = properties.getApiKey(); + if (StringUtils.isNotEmpty(apiKey)) { + headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); + } + + CollectionUtils.emptyIfNull(ips) + .forEach(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); + + return headers; + } + + private Future validateResponse(HttpClientResponse response) { + if (response.getStatusCode() != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new ValidationException("Invalid status code: %d", response.getStatusCode())); + } + + if (StringUtils.isBlank(response.getBody())) { + return Future.failedFuture(new ValidationException("Empty body")); + } + + return Future.succeededFuture(response); + } + + private TargetingResult parseResponse(HttpClientResponse httpResponse) { + return mapper.decodeValue(httpResponse.getBody(), TargetingResult.class); + } + + private void logError(Throwable exception, String url) { + final String errorPrefix = "Error occurred while sending HTTP request to the Optable url:"; + + final String error = errorPrefix + " %s with message: %s".formatted(url, exception.getMessage()); + conditionalLogger.warn(error, logSamplingRate); + + logger.debug(errorPrefix + " {}", exception, url); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java new file mode 100644 index 00000000000..e7e8bc3e452 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -0,0 +1,64 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +public class CachedAPIClient implements APIClient { + + private final APIClient apiClient; + private final Cache cache; + private final boolean isCircuitBreakerEnabled; + + public CachedAPIClient(APIClient apiClient, Cache cache, boolean isCircuitBreakerEnabled) { + this.apiClient = Objects.requireNonNull(apiClient); + this.cache = Objects.requireNonNull(cache); + this.isCircuitBreakerEnabled = isCircuitBreakerEnabled; + } + + public Future getTargeting(OptableTargetingProperties properties, + Query query, + List ips, + String userAgent, + Timeout timeout) { + + final CacheProperties cacheProperties = properties.getCache(); + if (!cacheProperties.isEnabled()) { + return apiClient.getTargeting(properties, query, ips, userAgent, timeout); + } + + final String tenant = properties.getTenant(); + final String origin = properties.getOrigin(); + + return cache.get(createCachingKey(tenant, origin, ips, query, true)) + .recover(ignore -> apiClient.getTargeting(properties, query, ips, userAgent, timeout) + .recover(throwable -> isCircuitBreakerEnabled + ? Future.succeededFuture(new TargetingResult(null, null)) + : Future.failedFuture(throwable)) + .compose(result -> cache.put( + createCachingKey(tenant, origin, ips, query, false), + result, + cacheProperties.getTtlseconds()) + .otherwiseEmpty() + .map(result))); + } + + private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { + return "%s:%s:%s:%s".formatted( + tenant, + origin, + ips.getFirst(), + encodeQuery + ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) + : query.getIds()); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java new file mode 100644 index 00000000000..99f24ea4bc5 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -0,0 +1,257 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.gpp.encoder.GppModel; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; +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.impl.headers.HeadersMultiMap; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.gpp.model.GppContext; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.Privacy; +import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +public abstract class BaseOptableTest { + + protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); + + protected final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); + + protected ModuleContext givenModuleContext() { + return givenModuleContext(null); + } + + protected ModuleContext givenModuleContext(List audiences) { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setTargeting(audiences); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + return moduleContext; + } + + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { + final GppModel gppModel = new GppModel(); + final TcfContext tcfContext = TcfContext.builder().build(); + final GppContext gppContext = new GppContext( + GppContext.Scope.of(gppModel, Set.of(1)), + GppContext.Regions.builder().build()); + + return AuctionContext.builder() + .bidRequest(givenBidRequest()) + .activityInfrastructure(activityInfrastructure) + .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) + .gppContext(gppContext) + .timeoutContext(TimeoutContext.of(0, timeout, 1)) + .build(); + } + + protected BidRequest givenBidRequest() { + return givenBidRequestWithUserEids(null); + } + + protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build(); + } + + protected BidRequest givenBidRequestWithUserEids(List eids) { + return BidRequest.builder() + .user(givenUser(eids)) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); + } + + protected BidRequest givenBidRequestWithUserData(List data) { + return BidRequest.builder() + .user(givenUserWithData(data)) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); + } + + protected BidResponse givenBidResponse() { + final ObjectNode targetingNode = mapper.createObjectNode(); + targetingNode.set("attribute1", TextNode.valueOf("value1")); + targetingNode.set("attribute2", TextNode.valueOf("value1")); + final ObjectNode bidderNode = mapper.createObjectNode(); + bidderNode.set("targeting", targetingNode); + final ObjectNode bidExtNode = mapper.createObjectNode(); + bidExtNode.set("prebid", bidderNode); + + return BidResponse.builder() + .seatbid(List.of(SeatBid.builder() + .bid(List.of(Bid.builder().ext(bidExtNode).build())) + .build())) + .build(); + } + + protected TargetingResult givenTargetingResultWithEids(List eids) { + return givenTargetingResult(eids, null); + } + + protected TargetingResult givenTargetingResultWithData(List data) { + return givenTargetingResult(null, data); + } + + protected TargetingResult givenTargetingResult() { + return givenTargetingResult( + List.of(Eid.builder() + .source("source") + .uids(List.of(Uid.builder().id("id").build())) + .build()), + List.of(Data.builder() + .id("id") + .segment(List.of(Segment.builder().id("id").build())) + .build())); + } + + protected TargetingResult givenTargetingResult(List eids, List data) { + return new TargetingResult( + List.of(new Audience( + "provider", + List.of(new AudienceId("id")), + "keyspace", + 1)), + new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data))); + } + + protected TargetingResult givenEmptyTargetingResult() { + return new TargetingResult(Collections.emptyList(), new Ortb2(null)); + } + + protected User givenUser() { + return givenUser(null); + } + + protected User givenUser(List eids) { + final ObjectNode optable = mapper.createObjectNode(); + optable.set("email", TextNode.valueOf("email")); + optable.set("phone", TextNode.valueOf("phone")); + optable.set("zip", TextNode.valueOf("zip")); + optable.set("vid", TextNode.valueOf("vid")); + + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", optable); + + return User.builder() + .eids(eids) + .geo(Geo.builder().country("country-u").region("region-u").build()) + .ext(extUser) + .build(); + } + + protected User givenUserWithData(List data) { + return User.builder() + .data(data) + .build(); + } + + protected Device givenDevice() { + return Device.builder().geo(Geo.builder().country("country-d").region("region-d").build()).build(); + } + + protected HttpClientResponse givenSuccessHttpResponse(String fileName) { + final MultiMap headers = HeadersMultiMap.headers().add("Content-Type", "application/json"); + return HttpClientResponse.of(HttpStatus.SC_OK, headers, givenBodyFromFile(fileName)); + } + + protected HttpClientResponse givenFailHttpResponse(String fileName) { + return givenFailHttpResponse(HttpStatus.SC_BAD_REQUEST, fileName); + } + + protected HttpClientResponse givenFailHttpResponse(int statusCode, String fileName) { + return HttpClientResponse.of(statusCode, null, givenBodyFromFile(fileName)); + } + + protected String givenBodyFromFile(String fileName) { + InputStream inputStream = null; + try { + inputStream = Files.newInputStream(Paths.get("src/test/resources/" + fileName)); + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + protected OptableTargetingProperties givenOptableTargetingProperties(boolean enableCache) { + return givenOptableTargetingProperties("key", "accountId", "origin", enableCache); + } + + protected OptableTargetingProperties givenOptableTargetingProperties(String key, boolean enableCache) { + return givenOptableTargetingProperties(key, "accountId", "origin", enableCache); + } + + protected OptableTargetingProperties givenOptableTargetingProperties(String key, + String tenant, + String origin, + boolean enableCache) { + final CacheProperties cacheProperties = new CacheProperties(); + cacheProperties.setEnabled(enableCache); + + final OptableTargetingProperties optableTargetingProperties = new OptableTargetingProperties(); + optableTargetingProperties.setApiEndpoint("endpoint"); + optableTargetingProperties.setTenant(tenant); + optableTargetingProperties.setOrigin(origin); + optableTargetingProperties.setApiKey(key); + optableTargetingProperties.setPpidMapping(Map.of("c", "id")); + optableTargetingProperties.setAdserverTargeting(true); + optableTargetingProperties.setTimeout(100L); + optableTargetingProperties.setCache(cacheProperties); + + return optableTargetingProperties; + } + + protected Query givenQuery() { + return Query.of("?que", "ry"); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java new file mode 100644 index 00000000000..af4a809df78 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -0,0 +1,145 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.BidResponse; +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.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +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.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { + + private ConfigResolver configResolver; + private AuctionResponseHook target; + + @Mock + private AuctionResponsePayload auctionResponsePayload; + @Mock(strictness = LENIENT) + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + target = new OptableTargetingAuctionResponseHook( + configResolver, + mapper, + jsonMerger); + } + + @Test + public void shouldHaveCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-auction-response-hook"); + + } + + @Test + public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags().activities().getFirst() + .results().getFirst().values().get("reason")).isNotNull(); + assertThat(result.errors()).isNull(); + } + + @Test + public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + + assertThat(targeting) + .isNotNull() + .hasSize(3); + + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); + } + + @Test + public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)))); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + } + + private ObjectNode givenAccountConfig(boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..008262b8a3e --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -0,0 +1,256 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +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.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { + + private ConfigResolver configResolver; + + @Mock + private OptableTargeting optableTargeting; + + @Mock + private UserFpdActivityMask userFpdActivityMask; + + private OptableTargetingProcessedAuctionRequestHook target; + + @Mock + private AuctionRequestPayload auctionRequestPayload; + + @Mock + private AuctionInvocationContext invocationContext; + + @Mock + private ActivityInfrastructure activityInfrastructure; + + @Mock + private Timeout timeout; + + @BeforeEach + public void setUp() { + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + optableTargeting, + userFpdActivityMask, + 0.01); + + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.timeout()).thenReturn(timeout); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(timeout.remaining()).thenReturn(1000L); + } + + @Test + public void shouldHaveRightCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook"); + } + + @Test + public void shouldReturnResultWithPBSAnalyticsTags() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + assertThat(result.analyticsTags().activities().getFirst() + .results().getFirst().values().get("execution-time")).isNotNull(); + } + + @Test + public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + } + + @Test + public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { + // given + configResolver = new ConfigResolver( + mapper, + jsonMerger, + givenOptableTargetingProperties("key", "tenant", null, false)); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + optableTargeting, + userFpdActivityMask, + 0.01); + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", null, true)); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat((ModuleContext) result.moduleContext()) + .extracting(it -> it.getEnrichRequestStatus().getStatus()) + .isEqualTo(Status.FAIL); + } + + @Test + public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { + // given + configResolver = new ConfigResolver( + mapper, + jsonMerger, + givenOptableTargetingProperties("key", null, "origin", false)); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + optableTargeting, + userFpdActivityMask, + 0.01); + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", null, null, true)); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat((ModuleContext) result.moduleContext()) + .extracting(it -> it.getEnrichRequestStatus().getStatus()) + .isEqualTo(Status.FAIL); + } + + @Test + public void shouldReturnResultWithCleanedUpUserExtOptableTag() { + // given + when(invocationContext.timeout()).thenReturn(timeout); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + final ObjectNode optable = (ObjectNode) result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest() + .getUser().getExt().getProperty("optable"); + + assertThat(optable).isNull(); + } + + @Test + public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())).thenReturn(Future.succeededFuture(null)); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + } + + private ObjectNode givenAccountConfig(boolean cacheEnabled) { + return givenAccountConfig("key", "tenant", "origin", cacheEnabled); + } + + private ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java new file mode 100644 index 00000000000..51c5c33890f --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java @@ -0,0 +1,74 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuctionResponseValidatorTest { + + @Test + public void shouldReturnNobidStatusWhenBidResponseIsEmpty() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + + // when + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.FAIL, EnrichmentStatus::getStatus) + .returns(Reason.NOBID, EnrichmentStatus::getReason); + } + + @Test + public void shouldReturnNoKeywordsStatusWhenTargetingHasNoIds() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + + // when + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.FAIL, EnrichmentStatus::getStatus) + .returns(Reason.NOBID, EnrichmentStatus::getReason); + } + + @Test + public void shouldReturnSuccessStatus() { + // given + final BidResponse bidResponse = BidResponse.builder() + .seatbid(List.of(SeatBid.builder() + .bid(List.of(Bid.builder().build())) + .build())) + .build(); + + // when + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.SUCCESS, EnrichmentStatus::getStatus) + .returns(Reason.NONE, EnrichmentStatus::getReason); + } + + private static List givenTargeting() { + return List.of(new Audience("provider", List.of(new AudienceId("id")), "keyspace", 1)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java new file mode 100644 index 00000000000..b6d72748fbd --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidRequestCleanerTest extends BaseOptableTest { + + @Test + public void shouldRemoveUserExtOptableTag() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + bidRequest.user(givenUser()))); + + // when + final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); + + // then + assertThat(result).extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getExt) + .extracting(it -> it.getProperty("optable")) + .isEqualTo(null); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java new file mode 100644 index 00000000000..71ee889008f --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -0,0 +1,547 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.as; +import static org.assertj.core.api.Assertions.assertThat; + +public class BidRequestEnricherTest extends BaseOptableTest { + + private final OptableTargetingProperties targetingProperties = new OptableTargetingProperties(); + + @Test + public void shouldReturnOriginBidRequestWhenNoTargetingResults() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(null, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } + + @Test + public void shouldNotFailIfBidRequestIsNull() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(null); + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNull(); + } + + @Test + public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + } + + @Test + public void shouldNotAddEidWhenSourceAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("inserter", "source", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids).filteredOn(it -> it.getSource().equals("source")).hasSize(1); + } + + @Test + public void shouldAddEidWhenSourceIsNotAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(3); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2", "source3"); + } + + @Test + public void shouldSkipEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id"); + } + + @Test + public void shouldMergeEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsMerge(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .contains("id", "id2"); + } + + @Test + public void shouldRemoveEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsIgnore(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1"); + } + + @Test + public void shouldRemoveEidWhenOptableSourceIsAlreadyPresentAndEmptyTargeting() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of()); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsIgnore(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1"); + } + + @Test + public void shouldReplaceEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsReplace(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id2"); + } + + @Test + public void shouldReplaceEidWhenOptableSourceIsPresentInBothMergeAndReplaceLists() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsReplace(Set.of("source2")); + properties.setOptableInserterEidsMerge(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id2"); + } + + @Test + public void shouldNotMergeOriginEidsWithTheSameSource() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(3); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source", "source", "source3"); + } + + @Test + public void shouldApplyOriginEidsWhenTargetingIsEmpty() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids).extracting(Eid::getSource).containsExactly("source3"); + } + + @Test + public void shouldApplyTargetingEidsWhenOriginListIsEmpty() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids).extracting(Eid::getSource).containsExactly("source", "source1"); + } + + @Test + public void shouldNotApplyEidsWhenOriginAndTargetingEidsAreEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids).isEmpty(); + } + + @Test + public void shouldMergeDataWithTheSameId() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id3", "value3"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getId).containsExactly("id", "id"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1", "id3"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id2", "id3"); + }); + } + + @Test + public void shouldMergeDistinctSegmentsWithinTheSameData() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id1", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id1", "value3"))), + givenData("id", List.of(givenSegment("id4", "value4"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getId).containsExactly("id", "id1"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1", "id4"); + assertThat(segments.getFirst()).filteredOn(it -> it.getId().equals("id1")) + .extracting(Segment::getValue).containsExactly("value1"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id2"); + }); + } + + @Test + public void shouldAppendDataWithNewId() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id1", List.of(givenSegment("id3", "value3"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(3); + assertThat(data).extracting(Data::getId).containsExactly("id", "id", "id1"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1"); + assertThat(segments.get(1)).extracting(Segment::getId).containsExactly("id2"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id3"); + }); + } + + @Test + public void shouldApplyOriginDataWhenTargetingIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1"); + assertThat(segments.get(1)).extracting(Segment::getId).containsExactly("id2"); + }); + } + + @Test + public void shouldApplyTargetingDataWhenOriginIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(1); + assertThat(data).flatMap(Data::getSegment).extracting(Segment::getId).containsExactly("id1"); + } + + @Test + public void shouldApplyNothingWhenOriginAndTargetingDataAreEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data).isEmpty(); + } + + @Test + public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + final TargetingResult targetingResult = givenEmptyTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } + + private Eid givenEid(String inserter, String source, List uids, ObjectNode ext) { + return Eid.builder() + .inserter(inserter) + .source(source) + .uids(uids) + .ext(ext) + .build(); + } + + private Uid givenUid(String id, Integer atype, ObjectNode ext) { + return Uid.builder() + .id(id) + .atype(atype) + .ext(ext) + .build(); + } + + private Data givenData(String id, List segments) { + return Data.builder() + .id(id) + .segment(segments) + .build(); + } + + private Segment givenSegment(String id, String value) { + return Segment.builder() + .id(id) + .value(value) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java new file mode 100644 index 00000000000..8b2e4b14432 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidResponseEnricherTest extends BaseOptableTest { + + @Test + public void shouldEnrichBidResponseByTargetingKeywords() { + // given + final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); + + // when + final AuctionResponsePayload result = BidResponseEnricher.of(givenTargeting(), mapper, jsonMerger) + .apply(auctionResponsePayload); + final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId,audienceId2"); + } + + @Test + public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { + // given + final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); + + // when + final AuctionResponsePayload result = BidResponseEnricher.of(null, mapper, jsonMerger) + .apply(auctionResponsePayload); + final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace")).isNull(); + } + + private static List givenTargeting() { + return List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId"), new AudienceId("audienceId2")), + "keyspace", + 1)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java new file mode 100644 index 00000000000..1917fd6ca27 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import io.vertx.core.Future; +import org.assertj.core.api.Assertions; +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CacheTest { + + @Mock + private PbcStorageService pbcStorageService; + + @Spy + private final JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); + + private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); + + private Cache target; + + @BeforeEach + public void setUp() { + target = new Cache(pbcStorageService, jacksonMapper); + } + + @Test + public void cacheShouldNotCallMapperIfNoEntry() { + // given + when(pbcStorageService.retrieveEntry(any(), any(), any())) + .thenReturn(Future.succeededFuture(ModuleCacheResponse.empty())); + + // when + final TargetingResult result = target.get("key").result(); + + // then + Assertions.assertThat(result).isNull(); + verify(jacksonMapper, times(0)).decodeValue(anyString(), eq(TargetingResult.class)); + } + + @Test + public void cacheShouldReturnEntry() { + // given + final TargetingResult targetingResult = givenTargetingResult(); + when(pbcStorageService.retrieveEntry(any(), any(), any())) + .thenReturn(Future.succeededFuture(ModuleCacheResponse.of( + "key", + StorageDataType.TEXT, + mapper.encodeToString(targetingResult)))); + + // when + final TargetingResult result = target.get("key").result(); + + // then + Assertions.assertThat(result) + .isNotNull() + .isEqualTo(targetingResult); + + verify(jacksonMapper, times(1)).decodeValue(anyString(), eq(TargetingResult.class)); + } + + @Test + public void cacheShouldStoreEntry() { + // given + final TargetingResult targetingResult = givenTargetingResult(); + + // when + when(pbcStorageService.storeEntry(any(), any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture()); + final boolean result = target.put("key", targetingResult, 86400).succeeded(); + + // then + Assertions.assertThat(result).isTrue(); + verify(pbcStorageService, times(1)).storeEntry( + eq("key"), + eq(mapper.encodeToString(targetingResult)), + eq(StorageDataType.TEXT), + eq(86400), + any(), + any()); + } + + private TargetingResult givenTargetingResult() { + return new TargetingResult( + List.of(new Audience( + "provider", + List.of(new AudienceId("1")), + "keyspace", + 0)), + new Ortb2(new User(null, null))); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java new file mode 100644 index 00000000000..39693661629 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -0,0 +1,124 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class IdsMapperTest { + + private final ObjectMapper objectMapper = ObjectMapperProvider.mapper(); + + private IdsMapper target; + + private final Map ppidMapping = Map.of("test.com", "c"); + + @BeforeEach + public void setUp() { + target = new IdsMapper(objectMapper, 0.01); + } + + @Test + public void shouldMapBidRequestToAllPossibleIds() { + //given + final BidRequest bidRequest = givenBidRequestWithEids(Map.of( + "id5-sync.com", "id5_id", + "test.com", "test_id", + "utiq.com", "utiq_id")); + + // when + final List ids = target.toIds(bidRequest, ppidMapping); + + // then + assertThat(ids).isNotNull() + .contains(Id.of(Id.EMAIL, "email")) + .contains(Id.of(Id.PHONE, "123")) + .contains(Id.of(Id.ZIP, "321")) + .contains(Id.of(Id.OPTABLE_VID, "vid")) + .contains(Id.of(Id.GOOGLE_GAID, "ifa")) + .doesNotContain(Id.of(Id.APPLE_IDFA, "ifa")) + .contains(Id.of(Id.ID5, "id5_id")) + .contains(Id.of(Id.UTIQ, "utiq_id")) + .contains(Id.of("c", "test_id")); + } + + @Test + public void shouldMapNothing() { + //given + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder); + + // when + final List ids = target.toIds(bidRequest, ppidMapping); + + // then + assertThat(ids).isNotNull(); + } + + private BidRequest givenBidRequestWithEids(Map eids) { + final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", extUserOptable); + + final User user = givenUser(userBuilder -> userBuilder.eids(toEids(eids)).ext(extUser)); + return givenBidRequest(builder -> builder.device(givenDevice()).user(user)); + } + + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .id("requestId") + .imp(singletonList(Imp.builder() + .id("impId") + .build()))) + .build(); + } + + private ExtUserOptable givenOptable() { + return ExtUserOptable.builder() + .email("email") + .phone("123") + .zip("321") + .vid("vid") + .build(); + } + + private Device givenDevice() { + return Device.builder() + .ip("127.0.0.1") + .ipv6("0:0:0:0:0:0:0:1") + .lmt(0) + .os("android") + .ifa("ifa") + .build(); + } + + private User givenUser(UnaryOperator userCustomizer) { + return userCustomizer.apply(User.builder()).build(); + } + + private List toEids(Map eids) { + return eids.entrySet() + .stream() + .map(it -> Eid.builder() + .source(it.getKey()) + .uids(List.of(Uid.builder().id(it.getValue()).build())) + .build()) + .toList(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java new file mode 100644 index 00000000000..de2c01948fb --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -0,0 +1,115 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.gpp.encoder.GppModel; +import com.iab.openrtb.request.BidRequest; +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.gpp.model.GppContext; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.Privacy; +import org.prebid.server.privacy.model.PrivacyContext; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableAttributesResolverTest extends BaseOptableTest { + + @Mock(strictness = LENIENT) + private TcfContext tcfContext; + + @Mock(strictness = LENIENT) + private GppContext gppContext; + + @Mock + private OptableTargetingProperties properties; + + @BeforeEach + public void setUp() { + when(properties.getTimeout()).thenReturn(100L); + } + + @Test + public void shouldResolveTcfAttributesWhenConsentIsValid() { + // given + final GppModel gppModel = mock(); + when(tcfContext.isConsentValid()).thenReturn(true); + when(tcfContext.isInGdprScope()).thenReturn(true); + when(tcfContext.getConsentString()).thenReturn("consent"); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(true, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGdprConsent); + } + + @Test + public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { + // given + final GppModel gppModel = mock(); + when(tcfContext.isConsentValid()).thenReturn(false); + when(tcfContext.getConsentString()).thenReturn("consent"); + when(tcfContext.getIpAddress()).thenReturn("8.8.8.8"); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(false, OptableAttributes::isGdprApplies) + .returns(null, OptableAttributes::getGdprConsent) + .returns(List.of("8.8.8.8"), OptableAttributes::getIps); + } + + @Test + public void shouldResolveGppAttributes() { + // given + final GppModel gppModel = mock(); + when(tcfContext.isConsentValid()).thenReturn(false); + when(tcfContext.getConsentString()).thenReturn("consent"); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(false, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGpp) + .returns(Set.of(1), OptableAttributes::getGppSid); + } + + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) + .gppContext(gppContext) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java new file mode 100644 index 00000000000..4533ef073ae --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -0,0 +1,97 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +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.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableTargetingTest extends BaseOptableTest { + + @Mock + private IdsMapper idsMapper; + + @Mock(strictness = Mock.Strictness.LENIENT) + private Cache cache; + + @Mock + private APIClientImpl apiClient; + + private OptableTargeting target; + + @Mock + private Timeout timeout; + + @BeforeEach + public void setUp() { + final CachedAPIClient cachingAPIClient = new CachedAPIClient(apiClient, cache, false); + target = new OptableTargeting(idsMapper, cachingAPIClient); + } + + @Test + public void shouldCallNonCachedAPIClient() { + // given + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + final BidRequest bidRequest = givenBidRequest(); + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + final OptableAttributes optableAttributes = givenOptableAttributes(); + + // when + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, timeout); + + // then + assertThat(targetingResult.result()).isNotNull(); + verify(apiClient).getTargeting(any(), any(), any(), any(), any()); + } + + @Test + public void shouldUseCachedAPIClient() { + // given + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(cache.get(any())).thenReturn(Future.failedFuture(new NullPointerException())); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + final BidRequest bidRequest = givenBidRequest(); + final OptableTargetingProperties properties = givenOptableTargetingProperties(true); + final OptableAttributes optableAttributes = givenOptableAttributes(); + + // when + target.getTargeting(properties, bidRequest, optableAttributes, timeout); + + // then + verify(cache).get(any()); + verify(apiClient).getTargeting(any(), any(), any(), any(), any()); + } + + private OptableAttributes givenOptableAttributes() { + return OptableAttributes.builder() + .gpp("gpp") + .ips(List.of("8.8.8.8")) + .gppSid(Set.of(2)) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java new file mode 100644 index 00000000000..0359578ee75 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -0,0 +1,122 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueryBuilderTest { + + private final OptableAttributes optableAttributes = givenOptableAttributes(); + + private final String idPrefixOrder = "c,c1"; + + @Test + public void shouldSeparateAttributesFromIds() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + + // then + assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); + } + + @Test + public void shouldBuildFullQueryString() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + + // then + assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); + assertThat(query.toQueryString()) + .isEqualTo("&id=e%3Aemail&id=p%3A123&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); + } + + @Test + public void shouldBuildQueryStringWhenHaveIds() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + + // then + assertThat(query).contains("e%3Aemail", "p%3A123"); + } + + @Test + public void shouldBuildQueryStringWithExtraAttributes() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + + // then + assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); + } + + @Test + public void shouldBuildQueryStringWithRightOrder() { + // given + final List ids = List.of( + Id.of(Id.ID5, "ID5"), + Id.of(Id.EMAIL, "email"), + Id.of("c1", "123"), + Id.of("c", "234")); + + // when + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + + // then + assertThat(query).startsWith("&id=c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); + } + + @Test + public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { + // given + final List ids = List.of(); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .build(); + + // when + final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + + // then + assertThat(query).isNotNull(); + assertThat(query.toQueryString()).isEqualTo("&gdpr=0&osdk=prebid-server"); + } + + @Test + public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { + // given + final List ids = List.of(); + final OptableAttributes attributes = OptableAttributes.builder().build(); + + // when + final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + + // then + assertThat(query).isNull(); + } + + private OptableAttributes givenOptableAttributes() { + return OptableAttributes.builder() + .timeout(100L) + .gdprApplies(true) + .gdprConsent("tcf") + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java new file mode 100644 index 00000000000..d147d7d4294 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java @@ -0,0 +1,254 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import org.apache.http.HttpStatus; +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.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.HttpClient; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class APIClientImplTest extends BaseOptableTest { + + @Mock + private HttpClient httpClient; + + private final JacksonMapper jacksonMapper = new JacksonMapper(mapper); + + private APIClient target; + + @Mock + private Timeout timeout; + + @BeforeEach + public void setUp() { + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, 100); + } + + @Test + public void shouldReturnTargetingResult() { + // given + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(result.result()).isNotNull(); + final TargetingResult res = result.result(); + final User user = res.getOrtb2().getUser(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("uid_id1"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("segment_id"); + } + + @Test + public void shouldReturnNullWhenEndpointRespondsWithError() { + // given + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenEndpointRespondsWithWrongData() { + // given + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenHttpClientIsCrashed() { + // given + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.failedFuture(new NullPointerException())); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenInternalErrorOccurs() { + // given + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { + // given + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, 10); + + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting(givenOptableTargetingProperties(false), + givenQuery(), List.of("8.8.8.8"), "user agent", timeout); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(HttpUtil.AUTHORIZATION_HEADER)).isEqualTo("Bearer key"); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldBuildApiUrlByReplacingTenantAndOriginMacros() { + // given + target = new APIClientImpl( + "http://endpoint.optable.com?t={{TENANT}}&o={{ORIGIN}}", + httpClient, + jacksonMapper, + 10); + + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting(givenOptableTargetingProperties(false), + givenQuery(), List.of("8.8.8.8"), "user agent", timeout); + + // then + final ArgumentCaptor endpointCaptor = ArgumentCaptor.forClass(String.class); + verify(httpClient).get(endpointCaptor.capture(), any(), anyLong()); + assertThat(endpointCaptor.getValue()) + .isEqualTo("http://endpoint.optable.com?t=accountId&o=origin?query"); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { + // given + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(null, false), + givenQuery(), + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(HttpUtil.AUTHORIZATION_HEADER)).isNull(); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldPassThroughIpAddressesAndUserAgent() { + // given + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8", "2001:4860:4860::8888"), + "user agent", + timeout); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); + final MultiMap headers = headersCaptor.getValue(); + assertThat(headers.getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) + .contains("8.8.8.8", "2001:4860:4860::8888"); + assertThat(headers.get(HttpUtil.USER_AGENT_HEADER)).isEqualTo("user agent"); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotPassThroughIpAddressAndUserAgentWhenNotSpecified() { + // given + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + null, + null, + timeout); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); + final MultiMap headers = headersCaptor.getValue(); + assertThat(headers.get(HttpUtil.X_FORWARDED_FOR_HEADER)).isNull(); + assertThat(headers.get(HttpUtil.USER_AGENT_HEADER)).isNull(); + assertThat(result.result()).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java new file mode 100644 index 00000000000..e624fa56a8c --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -0,0 +1,173 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +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.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CachedAPIClientTest extends BaseOptableTest { + + @Mock + private APIClientImpl apiClient; + + @Mock(strictness = Mock.Strictness.LENIENT) + private Cache cache; + + private CachedAPIClient target; + + @Mock(strictness = Mock.Strictness.LENIENT) + private Timeout timeout; + + @BeforeEach + public void setUp() { + target = new CachedAPIClient(apiClient, cache, false); + when(timeout.remaining()).thenReturn(1000L); + } + + @Test + public void shouldCallAPIAndAddTargetingResultsToCache() { + // given + when(cache.get(any())).thenReturn(Future.failedFuture("error")); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); + final Query query = givenQuery(); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() { + // given + when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); + final Query query = givenQuery(); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any()); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldUseCachedResult() { + // given + when(cache.get(any())).thenReturn(Future.succeededFuture(givenTargetingResult())); + final Query query = givenQuery(); + + // when + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache, times(1)).get(any()); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), any(), any()); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldNotFailWhenApiClientIsFailed() { + // given + final Query query = givenQuery(); + when(cache.get(any())).thenReturn(Future.failedFuture("empty")); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.failedFuture(new NullPointerException())); + + // when + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + assertThat(targetingResult.result()).isNull(); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { + // given + final Query query = givenQuery(); + when(cache.get(any())).thenReturn(Future.failedFuture("empty")); + when(apiClient.getTargeting(any(), any(), any(), any(), any())) + .thenReturn(Future.failedFuture(new NullPointerException())); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); + + // when + target = new CachedAPIClient(apiClient, cache, true); + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final TargetingResult result = targetingResult.result(); + assertThat(result).isNotNull(); + assertThat(result.getOrtb2()).isNull(); + assertThat(result.getAudience()).isNull(); + verify(cache, times(1)).put(any(), eq(targetingResult.result()), anyInt()); + } +} diff --git a/extra/modules/optable-targeting/src/test/resources/error_response.json b/extra/modules/optable-targeting/src/test/resources/error_response.json new file mode 100644 index 00000000000..4250099e7ba --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/error_response.json @@ -0,0 +1 @@ +{"details": "Error message"} diff --git a/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json b/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json new file mode 100644 index 00000000000..ec6816d6f25 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json @@ -0,0 +1 @@ +Plain text diff --git a/extra/modules/optable-targeting/src/test/resources/targeting_response.json b/extra/modules/optable-targeting/src/test/resources/targeting_response.json new file mode 100644 index 00000000000..a5959de45c9 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/targeting_response.json @@ -0,0 +1,62 @@ +{ + "user": [ + + ], + "audience": [ + { + "provider": "optable.co", + "ids": [ + { + "id": "audience_id" + } + ], + "keyspace": "keyspace", + "rtb_segtax": 1 + } + ], + "ortb2": { + "user": { + "data": [ + { + "id": "data_id", + "segment": [ + { + "id": "segment_id" + } + ] + } + ], + "eids": [ + { + "source": "eid_source1", + "uids": [ + { + "id": "uid_id1", + "atype": 3, + "ext": { + "advertising_token": "advertising_token", + "refresh_token": "refresh_token", + "identity_expires": 1739281106209, + "refresh_from": 1739025506209, + "refresh_expires": 1741613906209, + "refresh_response_key": "refresh_response_key" + } + } + ] + }, + { + "source": "eid_source2", + "uids": [ + { + "id": "uid_id2", + "atype": 3, + "ext": { + "stype": "cto_bundle_hem_api" + } + } + ] + } + ] + } + } +} diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml index 27bdb434d58..55107ec2223 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.39.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..1296b3e50c7 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"; @@ -98,10 +103,16 @@ public Result blockedAttributesFor(BidRequest bidRequest) { final Result cattaxComplement = blockedCattaxComplement(bidRequest); final Result> bapp = 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 = - blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest); + final Result>> btype = blockedAttributesForImps( + BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, BANNER_MEDIA_TYPE, bidRequest); + final Result>> bannerBattr = blockedAttributesForImps( + BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, BANNER_MEDIA_TYPE, bidRequest); + final Result>> videoBattr = blockedAttributesForImps( + BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, VIDEO_MEDIA_TYPE, bidRequest); + final Result>> audioBattr = blockedAttributesForImps( + BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, AUDIO_MEDIA_TYPE, 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); } @@ -198,19 +226,23 @@ private Integer blockedCattaxComplementFromConfig() { private Result>> blockedAttributesForImps(String attribute, Class attributeType, String fieldName, + String attributeMediaType, BidRequest bidRequest) { final Map> attributeValues = new HashMap<>(); final List> results = new ArrayList<>(); for (final Imp imp : bidRequest.getImp()) { - final Result> attributeForImp = blockedAttribute( - attribute, attributeType, fieldName, mediaTypesFrom(imp)); - - if (attributeForImp.hasValue()) { - attributeValues.put(imp.getId(), attributeForImp.getValue()); + final Set actualMediaTypes = mediaTypesFrom(imp); + if (actualMediaTypes.contains(attributeMediaType)) { + final Result> attributeForImp = blockedAttribute( + attribute, attributeType, fieldName, actualMediaTypes); + + if (attributeForImp.hasValue()) { + attributeValues.put(imp.getId(), attributeForImp.getValue()); + } + results.add(attributeForImp); } - results.add(attributeForImp); } return Result.of( @@ -218,6 +250,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 +414,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 +430,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..f34355dc9e4 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,9 @@ 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.Rejection; +import org.prebid.server.auction.model.BidRejection; 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 +19,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; @@ -84,7 +89,6 @@ public ExecutionResult block() { try { final List> blockedBidResults = bids.stream() - .sequential() .map(bid -> isBlocked(bid, accountConfigReader)) .toList(); @@ -96,11 +100,18 @@ public ExecutionResult block() { final BlockedBids blockedBids = !blockedBidIndexes.isEmpty() ? BlockedBids.of(blockedBidIndexes) : null; final List warnings = MergeUtils.mergeMessages(blockedBidResults); + final List rejectedBids = new ArrayList<>(); + if (blockedBids != null) { + blockedBidIndexes.forEach(index -> + rejectBlockedBid(rejectedBids, blockedBidResults.get(index).getValue(), bids.get(index))); + } + return ExecutionResult.builder() .value(blockedBids) .debugMessages(blockedBids != null ? debugMessages(blockedBidIndexes, blockedBidResults) : null) .warnings(warnings) .analyticsResults(toAnalyticsResults(blockedBidResults)) + .rejections(rejectedBids) .build(); } catch (InvalidAccountConfigurationException e) { return debugEnabled ? ExecutionResult.withError(e.getMessage()) : ExecutionResult.empty(); @@ -159,11 +170,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 +286,23 @@ private String debugEntryFor(int index, BlockingResult blockingResult) { blockingResult.getFailedChecks()); } + private void rejectBlockedBid(List rejections, BlockingResult blockingResult, BidderBid blockedBid) { + if (blockingResult.getBattrCheckResult().isFailed() + || blockingResult.getBappCheckResult().isFailed() + || blockingResult.getBcatCheckResult().isFailed()) { + + rejections.add(BidRejection.of( + blockedBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE)); + } + + if (blockingResult.getBadvCheckResult().isFailed()) { + rejections.add(BidRejection.of( + blockedBid, + BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED)); + } + } + private List toAnalyticsResults(List> blockedBidResults) { return blockedBidResults.stream() .map(Result::getValue) @@ -286,7 +333,7 @@ private Map toAnalyticsResultValues(BlockingResult blockingResul } final AttributeCheckResult bappResult = blockingResult.getBappCheckResult(); if (bappResult.isFailed()) { - values.put(BUNDLE_FIELD, bappResult.getFailedValues().get(0)); + values.put(BUNDLE_FIELD, bappResult.getFailedValues().getFirst()); } final AttributeCheckResult battrResult = blockingResult.getBattrCheckResult(); if (battrResult.isFailed()) { 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..78744e7c07f 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,11 +1,14 @@ 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; @@ -40,39 +43,99 @@ 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) { + if (banner == null) { + return null; + } + + final List existingBtype = banner.getBtype(); + final List existingBattr = banner.getBattr(); + + return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr) + ? banner.toBuilder() + .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype) + .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr) + .build() + : banner; + } + + private static Video updateVideo(Video video, List battr) { + if (video == null) { + return null; + } + + final List existingBattr = video.getBattr(); + return CollectionUtils.isEmpty(existingBattr) + ? video.toBuilder() + .battr(battr) + .build() + : video; + } + + private static Audio updateAudio(Audio audio, List battr) { + if (audio == null) { + return null; + } + + final List existingBattr = audio.getBattr(); + return CollectionUtils.isEmpty(existingBattr) + ? audio.toBuilder() + .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/ExecutionResult.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java index 5765dfb5452..9b767cdf264 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/model/ExecutionResult.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Value; +import org.prebid.server.auction.model.Rejection; import java.util.Collections; import java.util.List; @@ -22,6 +23,8 @@ public class ExecutionResult { List analyticsResults; + List rejections; + public boolean hasValue() { return value != null; } 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..e13deb60d3d 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 @@ -2,16 +2,16 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BlockedAttributesResolver; import org.prebid.server.hooks.modules.ortb2.blocking.core.RequestUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -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..94271ff7ca5 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 @@ -6,18 +6,18 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BidsBlocker; import org.prebid.server.hooks.modules.ortb2.blocking.core.ResponseUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; 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.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -72,7 +72,10 @@ public Future> call(BidderResponsePayloa .errors(blockedBidsResult.getErrors()) .warnings(blockedBidsResult.getWarnings()) .debugMessages(blockedBidsResult.getDebugMessages()) - .analyticsTags(toAnalyticsTags(blockedBidsResult.getAnalyticsResults())); + .analyticsTags(toAnalyticsTags(blockedBidsResult.getAnalyticsResults())) + .rejections(CollectionUtils.isEmpty(blockedBidsResult.getRejections()) + ? null + : Map.of(bidder, blockedBidsResult.getRejections())); if (blockedBidsResult.hasValue()) { final ResponseUpdater responseUpdater = ResponseUpdater.create(blockedBidsResult.getValue()); diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java deleted file mode 100644 index bd394217b21..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderRequestPayloadImpl implements BidderRequestPayload { - - BidRequest bidRequest; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java deleted file mode 100644 index 72d678c89a5..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderResponsePayloadImpl implements BidderResponsePayload { - - List bids; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java deleted file mode 100644 index 48be15fdf37..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -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; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - ModuleContext moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index 484489a5e6f..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 2971cc40d6e..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java deleted file mode 100644 index 5405799e25f..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java deleted file mode 100644 index 9f0432b9e2f..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} 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..48986d10d73 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; @@ -47,7 +48,7 @@ public class AccountConfigReaderTest { - private static final ObjectMapper mapper = new ObjectMapper() + private static final ObjectMapper MAPPER = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -97,7 +98,7 @@ public void blockedAttributesForShouldReturnEmptyResultWhenNoBlockedAdomains() { @Test public void blockedAttributesForShouldReturnErrorWhenAttributesIsNotObject() { // given - final ObjectNode accountConfig = mapper.createObjectNode().put("attributes", 1); + final ObjectNode accountConfig = MAPPER.createObjectNode().put("attributes", 1); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then @@ -109,8 +110,8 @@ public void blockedAttributesForShouldReturnErrorWhenAttributesIsNotObject() { @Test public void blockedAttributesForShouldReturnErrorWhenBadvIsNotObject() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() .put("badv", 1)); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -123,9 +124,9 @@ public void blockedAttributesForShouldReturnErrorWhenBadvIsNotObject() { @Test public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() .put("blocked-adomain", 1))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -138,10 +139,10 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotArray() @Test public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotStringArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() .add(1) .add("domain2.com")))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -156,9 +157,9 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainsIsNotStringA @Test public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesIsNotObject() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() .put("action-overrides", 1))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -171,13 +172,13 @@ public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesIsNotObj @Test public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesBlockedAdomainIsNotObjectArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() .add(1) - .add(mapper.createObjectNode()))))); + .add(MAPPER.createObjectNode()))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then @@ -189,12 +190,12 @@ public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesBlockedA @Test public void blockedAttributesForShouldReturnErrorWhenOverridesHasNoConditions() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode()))))); + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode()))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then @@ -206,12 +207,12 @@ public void blockedAttributesForShouldReturnErrorWhenOverridesHasNoConditions() @Test public void blockedAttributesForShouldReturnErrorWhenConditionsIsNotObject() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() .put("conditions", 1)))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -224,32 +225,32 @@ public void blockedAttributesForShouldReturnErrorWhenConditionsIsNotObject() { @Test public void blockedAttributesForShouldReturnErrorWhenBadvActionOverridesBlockedAdomainConditionsIsEmpty() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode())))))); + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode())))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) .isInstanceOf(InvalidAccountConfigurationException.class) - .hasMessage("conditions field in account configuration must contain at least one of bidders or " + - "media-type"); + .hasMessage( + "conditions field in account configuration must contain at least one of bidders or media-type"); } @Test public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() .put("bidders", 1))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -262,14 +263,14 @@ public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotArray( @Test public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotStringArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() - .set("bidders", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() + .set("bidders", MAPPER.createArrayNode() .add(1) .add("abc")))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -284,13 +285,13 @@ public void blockedAttributesForShouldReturnErrorWhenConditionBiddersIsNotString @Test public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() .put("media-type", 1))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -303,14 +304,14 @@ public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotArra @Test public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotStringArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() - .set("media-type", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() + .set("media-type", MAPPER.createArrayNode() .add(1) .add("abc")))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -325,14 +326,14 @@ public void blockedAttributesForShouldReturnErrorWhenConditionMediaTypeIsNotStri @Test public void blockedAttributesForShouldReturnErrorWhenActionOverridesHasNoOverride() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() - .set("bidders", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() + .set("bidders", MAPPER.createArrayNode() .add("bidder1")))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -345,14 +346,14 @@ public void blockedAttributesForShouldReturnErrorWhenActionOverridesHasNoOverrid @Test public void blockedAttributesForShouldReturnErrorWhenBadvBlockedAdomainOverrideIsNotArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() - .set("bidders", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() + .set("bidders", MAPPER.createArrayNode() .add("bidder1"))) .put("override", 1)))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -366,16 +367,16 @@ public void blockedAttributesForShouldReturnErrorWhenBadvBlockedAdomainOverrideI @Test public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainOverrideIsNotStringArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("blocked-adomain", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode() - .set("bidders", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("blocked-adomain", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode() + .set("bidders", MAPPER.createArrayNode() .add("bidder1"))) - .set("override", mapper.createArrayNode() + .set("override", MAPPER.createArrayNode() .add(1) .add("abc"))))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -390,16 +391,16 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedAdomainOverrideIsNot @Test public void blockedAttributesForShouldReturnErrorWhenBlockedBannerTypeIsNotIntegerArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("btype", mapper.createObjectNode() - .set("blocked-banner-type", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("btype", MAPPER.createObjectNode() + .set("blocked-banner-type", MAPPER.createArrayNode() .add(1) .add("type2")))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then - assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest())) + assertThatThrownBy(() -> reader.blockedAttributesFor(request(imp -> imp.banner(Banner.builder().build())))) .isInstanceOf(InvalidAccountConfigurationException.class) .hasMessage("blocked-banner-type field in account configuration has unexpected type. " + "Expected class java.lang.Integer"); @@ -601,8 +602,8 @@ public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverride .banner(Banner.builder().build())))) .isEqualTo(Result.of( attributesWithBadv(singletonList("domain3.com")), - singletonList("More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner, video]"))); + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); } @Test @@ -631,7 +632,7 @@ public void blockedAttributesForShouldReturnResultWithoutWarningWhenMultipleSpec } @Test - public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverridesWhenMultipleSpecificAndCatchAllMatches() { + public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverridesOnMultipleMatches() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -660,8 +661,8 @@ public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverride .banner(Banner.builder().build())))) .isEqualTo(Result.of( attributesWithBadv(singletonList("domain3.com")), - singletonList("More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner, video]"))); + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); } @Test @@ -688,8 +689,8 @@ public void blockedAttributesForShouldReturnResultWithBadvAndWarningFromOverride .banner(Banner.builder().build())))) .isEqualTo(Result.of( attributesWithBadv(singletonList("domain5.com")), - singletonList("More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner, video]"))); + singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]"))); } @Test @@ -699,17 +700,23 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri .btype(Attribute.btypeBuilder() .actionOverrides(AttributeActionOverrides.blocked(asList( ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(1)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("video")), + Conditions.of(singletonList("bidder1"), singletonList("banner")), singletonList(2)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), + Conditions.of(singletonList("bidder1"), singletonList("video")), singletonList(3)), ArrayOverride.of( - Conditions.of(singletonList("bidder1"), singletonList("banner")), - singletonList(4))))) + Conditions.of(singletonList("bidder1"), singletonList("video")), + singletonList(4)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(5)), + ArrayOverride.of( + Conditions.of(singletonList("bidder1"), singletonList("audio")), + singletonList(6))))) .build()) .build())); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -717,24 +724,20 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri // when and then final Map> expectedBtype = new HashMap<>(); expectedBtype.put("impId1", singletonList(1)); - expectedBtype.put("impId2", singletonList(3)); assertThat(reader .blockedAttributesFor(BidRequest.builder() .imp(asList( - Imp.builder().id("impId1").video(Video.builder().build()).build(), - Imp.builder().id("impId2").banner(Banner.builder().build()).build())) + Imp.builder().id("impId1").banner(Banner.builder().build()).build(), + Imp.builder().id("impId2").video(Video.builder().build()).build())) .build())) .isEqualTo(Result.of( BlockedAttributes.builder().btype(expectedBtype).build(), - asList( - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [video]", - "More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner]"))); + List.of("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner]"))); } @Test - public void blockedAttributesForShouldReturnResultWithAllAttributes() { + public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() .badv(Attribute.badvBuilder() @@ -766,7 +769,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( @@ -777,20 +780,126 @@ public void blockedAttributesForShouldReturnResultWithAllAttributes() { 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() + assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").banner(Banner.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() .badv(singletonList("domain3.com")) .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").video(Video.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .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").audio(Audio.builder().build())))) + .isEqualTo(Result.withValue(BlockedAttributes.builder() + .badv(singletonList("domain3.com")) + .bcat(singletonList("cat3")) + .bapp(singletonList("app3")) + .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3)))) .build())); } @Test public void blockedAttributesForShouldNotReturnCattaxIfBidderSupportsLowerThan26() throws JsonProcessingException { // given - final ObjectNode accountConfig = (ObjectNode) mapper.readTree(""" + final ObjectNode accountConfig = (ObjectNode) MAPPER.readTree(""" { "attributes": { "bcat": { @@ -810,7 +919,7 @@ public void blockedAttributesForShouldNotReturnCattaxIfBidderSupportsLowerThan26 @Test public void blockedAttributesForShouldReturnCattaxFromRequestIfPresent() throws JsonProcessingException { // given - final ObjectNode accountConfig = (ObjectNode) mapper.readTree(""" + final ObjectNode accountConfig = (ObjectNode) MAPPER.readTree(""" { "attributes": { "bcat": { @@ -832,7 +941,7 @@ public void blockedAttributesForShouldReturnCattaxFromRequestIfPresent() throws @Test public void blockedAttributesForShouldReturnCattaxFromConfigIfNotPresentInRequest() throws JsonProcessingException { // given - final ObjectNode accountConfig = (ObjectNode) mapper.readTree(""" + final ObjectNode accountConfig = (ObjectNode) MAPPER.readTree(""" { "attributes": { "bcat": { @@ -854,9 +963,9 @@ public void blockedAttributesForShouldReturnCattaxFromConfigIfNotPresentInReques @Test public void responseBlockingConfigForShouldReturnErrorWhenDefaultEnforceBlocksIsNotBoolean() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() .put("enforce-blocks", 1))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -870,9 +979,9 @@ public void responseBlockingConfigForShouldReturnErrorWhenDefaultEnforceBlocksIs @Test public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainIsNotArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() .put("allowed-adomain-for-deals", 1))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -885,10 +994,10 @@ public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainI @Test public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainIsNotStringArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("allowed-adomain-for-deals", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("allowed-adomain-for-deals", MAPPER.createArrayNode() .add(1) .add("domain1.com")))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -903,10 +1012,10 @@ public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedAdomainI @Test public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedBlockedAttrIsNotIntegerArray() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("battr", mapper.createObjectNode() - .set("allowed-banner-attr-for-deals", mapper.createArrayNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("battr", MAPPER.createObjectNode() + .set("allowed-banner-attr-for-deals", MAPPER.createArrayNode() .add(1) .add("domain1.com")))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); @@ -921,13 +1030,13 @@ public void responseBlockingConfigForShouldReturnErrorWhenDefaultAllowedBlockedA @Test public void responseBlockingConfigForShouldReturnErrorWhenDealConditionsIsEmpty() { // given - final ObjectNode accountConfig = mapper.createObjectNode() - .set("attributes", mapper.createObjectNode() - .set("badv", mapper.createObjectNode() - .set("action-overrides", mapper.createObjectNode() - .set("allowed-adomain-for-deals", mapper.createArrayNode() - .add(mapper.createObjectNode() - .set("conditions", mapper.createObjectNode())))))); + final ObjectNode accountConfig = MAPPER.createObjectNode() + .set("attributes", MAPPER.createObjectNode() + .set("badv", MAPPER.createObjectNode() + .set("action-overrides", MAPPER.createObjectNode() + .set("allowed-adomain-for-deals", MAPPER.createArrayNode() + .add(MAPPER.createObjectNode() + .set("conditions", MAPPER.createObjectNode())))))); final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true); // when and then @@ -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(); }); @@ -1281,6 +1554,6 @@ private static BlockedAttributes attributesWithBadv(List badv) { } private static ObjectNode toObjectNode(ModuleConfig config) { - return mapper.valueToTree(config); + return MAPPER.valueToTree(config); } } 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..549d48e5ae4 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,10 @@ 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.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.BidRejection; 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 +19,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,10 +33,13 @@ import static java.util.Collections.singletonMap; import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.auction.model.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED; +import static org.prebid.server.auction.model.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE; +@ExtendWith(MockitoExtension.class) public class BidsBlockerTest { - private static final ObjectMapper mapper = new ObjectMapper() + private static final ObjectMapper MAPPER = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -42,7 +49,7 @@ public class BidsBlockerTest { 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(bids, ORTB_VERSION, null, null, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); @@ -51,11 +58,11 @@ public void shouldReturnEmptyResultWhenNoBlockingResponseConfig() { @Test public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidsBlocker blocker = bidsBlocker(bids, ORTB_VERSION, accountConfig, null, true); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.builder() @@ -66,14 +73,15 @@ public void shouldReturnEmptyResultWithErrorWhenInvalidAccountConfig() { @Test public void shouldReturnEmptyResultWithoutErrorWhenInvalidAccountConfigAndDebugDisabled() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidsBlocker blocker = bidsBlocker(bids, ORTB_VERSION, accountConfig, null, false); // when and then assertThat(blocker.block()).isEqualTo(ExecutionResult.empty()); + } @Test @@ -88,10 +96,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(bids, ORTB_VERSION, accountConfig, null, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -106,10 +115,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(bids, ORTB_VERSION, accountConfig, null, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -124,7 +134,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(bids, ORTB_VERSION, accountConfig, null, false); // when and then assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); @@ -142,10 +152,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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -160,10 +171,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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -176,12 +188,16 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTr .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.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( + singletonList(bid), ORTB_VERSION, accountConfig, blockedAttributes, false); // when and then - assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + assertThat(blocker.block()).satisfies(result -> { + hasValue(result, 0); + assertThat(result.getRejections()).containsOnly(BidRejection.of(bid, RESPONSE_REJECTED_ADVERTISER_BLOCKED)); + }); } @Test @@ -195,17 +211,18 @@ 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(bids, ORTB_VERSION, accountConfig, null, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() { // given final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); @@ -215,12 +232,59 @@ public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedBannerAttrForImp() .impid("impId2") .attr(singletonList(1)))); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() - .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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + + } + + @Test + public void shouldReturnEmptyResultWhenBidWithAttrAndNoBlockedVideoAttrForImp() { + // given + final ObjectNode accountConfig = toObjectNode(ModuleConfig.of(Attributes.builder() + .battr(Attribute.videoBattrBuilder() + .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.VIDEO, singletonMap("impId1", asList(1, 2)))) + .build(); + final BidsBlocker blocker = bidsBlocker(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + + } + + @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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); + + // when and then + assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -234,12 +298,14 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.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( + singletonList(bid), ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty); + } @Test @@ -253,12 +319,16 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExcep .build())); // when - final List bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))); + final BidderBid bid = bid(bidBuilder -> bidBuilder.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( + singletonList(bid), ORTB_VERSION, accountConfig, blockedAttributes, false); // when and then - assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + assertThat(blocker.block()).satisfies(result -> { + hasValue(result, 0); + assertThat(result.getRejections()).containsOnly(BidRejection.of(bid, RESPONSE_REJECTED_ADVERTISER_BLOCKED)); + }); } @Test @@ -272,14 +342,15 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() { .build())); // when - final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, true); + final BidderBid bid = bid(); + final BidsBlocker blocker = bidsBlocker(singletonList(bid), ORTB_VERSION, accountConfig, null, true); // when and then assertThat(blocker.block()).satisfies(result -> { assertThat(result.getValue()).isEqualTo(BlockedBids.of(singleton(0))); assertThat(result.getDebugMessages()).containsOnly( "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]"); + assertThat(result.getRejections()).containsOnly(BidRejection.of(bid, RESPONSE_REJECTED_INVALID_CREATIVE)); }); } @@ -294,11 +365,14 @@ public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebug .build())); // when - final List bids = singletonList(bid()); - final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, false); + final BidderBid bid = bid(); + final BidsBlocker blocker = bidsBlocker(singletonList(bid), ORTB_VERSION, accountConfig, null, false); // when and then - assertThat(blocker.block()).satisfies(result -> hasValue(result, 0)); + assertThat(blocker.block()).satisfies(result -> { + hasValue(result, 0); + assertThat(result.getRejections()).containsOnly(BidRejection.of(bid, RESPONSE_REJECTED_INVALID_CREATIVE)); + }); } @Test @@ -314,34 +388,35 @@ public void shouldReturnResultWithAnalyticsResults() { .bapp(Attribute.bappBuilder() .enforceBlocks(true) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .build()) .build())); + final BidderBid bid1 = bid(bid -> bid + .impid("impId1") + .adomain(asList("domain2.com", "domain3.com", "domain4.com")) + .bundle("app2")); + final BidderBid bid2 = bid(bid -> bid + .impid("impId2") + .cat(asList("cat2", "cat3", "cat4")) + .attr(asList(2, 3, 4))); + final BidderBid bid3 = bid(bid -> bid + .impid("impId1") + .adomain(singletonList("domain5.com")) + .cat(singletonList("cat5")) + .bundle("app5") + .attr(singletonList(5))); + // when - final List bids = asList( - bid(bid -> bid - .impid("impId1") - .adomain(asList("domain2.com", "domain3.com", "domain4.com")) - .bundle("app2")), - bid(bid -> bid - .impid("impId2") - .cat(asList("cat2", "cat3", "cat4")) - .attr(asList(2, 3, 4))), - bid(bid -> bid - .impid("impId1") - .adomain(singletonList("domain5.com")) - .cat(singletonList("cat5")) - .bundle("app5") - .attr(singletonList(5)))); + final List bids = asList(bid1, bid2, bid3); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() .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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -360,6 +435,11 @@ public void shouldReturnResultWithAnalyticsResults() { AnalyticsResult.of("success-blocked", analyticsResultValues1, "bidder1", "impId1"), AnalyticsResult.of("success-blocked", analyticsResultValues2, "bidder1", "impId2"), AnalyticsResult.of("success-allow", null, "bidder1", "impId1")); + + assertThat(result.getRejections()).containsOnly( + BidRejection.of(bid1, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid2, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid1, RESPONSE_REJECTED_ADVERTISER_BLOCKED)); }); } @@ -381,37 +461,44 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { .enforceBlocks(true) .allowedForDeals(singletonList("app2")) .build()) - .battr(Attribute.battrBuilder() + .battr(Attribute.bannerBattrBuilder() .enforceBlocks(true) .allowedForDeals(singletonList(2)) .build()) .build())); // when - final List bids = asList( - bid(bid -> bid.adomain(singletonList("domain1.com"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app1")), - bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app2")), - bid(bid -> bid - .adomain(singletonList("domain2.com")) - .cat(singletonList("cat2")) - .bundle("app2") - .attr(singletonList(1))), - bid(bid -> bid - .adomain(singletonList("domain2.com")) - .cat(singletonList("cat2")) - .bundle("app2") - .attr(singletonList(2))), - bid()); + final BidderBid bid1 = bid(bid -> bid.adomain(singletonList("domain1.com"))); + final BidderBid bid2 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))); + final BidderBid bid3 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))); + final BidderBid bid4 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app1")); + final BidderBid bid5 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2")); + final BidderBid bid6 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(1))); + final BidderBid bid7 = bid(bid -> bid + .adomain(singletonList("domain2.com")) + .cat(singletonList("cat2")) + .bundle("app2") + .attr(singletonList(2))); + final BidderBid bid8 = bid(); + + final List bids = asList(bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8); final BlockedAttributes blockedAttributes = BlockedAttributes.builder() .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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -422,6 +509,14 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() { "Bid 3 from bidder bidder1 has been rejected, failed checks: [bapp]", "Bid 5 from bidder bidder1 has been rejected, failed checks: [battr]", "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]"); + assertThat(result.getRejections()).containsOnly( + BidRejection.of(bid1, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid1, RESPONSE_REJECTED_ADVERTISER_BLOCKED), + BidRejection.of(bid2, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid4, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid6, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid8, RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of(bid8, RESPONSE_REJECTED_ADVERTISER_BLOCKED)); }); } @@ -443,7 +538,7 @@ 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(bids, ORTB_VERSION, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()) @@ -464,8 +559,8 @@ public void shouldPassBidIfCattaxIsNull() { // when 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); + final BidsBlocker blocker = bidsBlocker( + bids, OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()) @@ -488,8 +583,8 @@ public void shouldBlockBidIfCattaxNotEqualsAllowedCattax() { bid(bid -> bid.cattax(1)), 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); + final BidsBlocker blocker = bidsBlocker( + bids, OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -514,8 +609,8 @@ public void shouldBlockBidIfCattaxNotEquals1IfBlockedAttributesCattaxAbsent() { bid(bid -> bid.cattax(1)), 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); + final BidsBlocker blocker = bidsBlocker( + bids, OrtbVersion.ORTB_2_6, accountConfig, blockedAttributes, true); // when and then assertThat(blocker.block()).satisfies(result -> { @@ -544,7 +639,7 @@ private static BlockedAttributes attributesWithBadv(List badv) { } private static ObjectNode toObjectNode(ModuleConfig config) { - return mapper.valueToTree(config); + return MAPPER.valueToTree(config); } private static void isEmpty(ExecutionResult result) { @@ -560,4 +655,13 @@ private static void hasValue(ExecutionResult result, Integer... ind assertThat(result.getWarnings()).isNull(); assertThat(result.getDebugMessages()).isNull(); } + + private BidsBlocker bidsBlocker(List bids, + OrtbVersion ortbVersion, + ObjectNode accountConfig, + BlockedAttributes blockedAttributes, + boolean debugEnabled) { + + return BidsBlocker.create(bids, "bidder1", ortbVersion, accountConfig, blockedAttributes, debugEnabled); + } } 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..2bc1afeb68c 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; @@ -27,7 +27,7 @@ public class BlockedAttributesResolverTest { - private static final ObjectMapper mapper = new ObjectMapper() + private static final ObjectMapper MAPPER = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -36,7 +36,7 @@ public class BlockedAttributesResolverTest { @Test public void shouldReturnEmptyResultWhenInvalidAccountConfigurationAndDebugDisabled() { // given - final ObjectNode accountConfig = mapper.createObjectNode().put("block-lists", 1); + final ObjectNode accountConfig = MAPPER.createObjectNode().put("block-lists", 1); final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( emptyRequest(), "bidder1", ORTB_VERSION, accountConfig, false); @@ -47,7 +47,7 @@ public void shouldReturnEmptyResultWhenInvalidAccountConfigurationAndDebugDisabl @Test public void shouldReturnResultWithErrorWhenInvalidAccountConfiguration() { // given - final ObjectNode accountConfig = mapper.createObjectNode().put("attributes", 1); + final ObjectNode accountConfig = MAPPER.createObjectNode().put("attributes", 1); final BlockedAttributesResolver resolver = BlockedAttributesResolver.create( emptyRequest(), "bidder1", ORTB_VERSION, accountConfig, true); @@ -83,8 +83,8 @@ public void shouldReturnResultWithValueAndWarnings() { // when and then assertThat(resolver.resolve()).isEqualTo(ExecutionResult.builder() .value(BlockedAttributes.builder().badv(singletonList("domain3.com")).build()) - .warnings(singletonList("More than one conditions matches request. Bidder: bidder1, " + - "request media types: [banner, video]")) + .warnings(singletonList("More than one conditions matches request. Bidder: bidder1, " + + "request media types: [banner, video]")) .build()); } @@ -131,6 +131,6 @@ private static BidRequest request(UnaryOperator impCustomizer) { } private static ObjectNode toObjectNode(ModuleConfig config) { - return mapper.valueToTree(config); + return MAPPER.valueToTree(config); } } 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..1f03f82edb1 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,10 +348,130 @@ 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") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .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()); + } + + @Test + public void shouldNotUpdateMissingBanner() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(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") + .video(Video.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .video(Video.builder().battr(asList(3, 4)).build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingVideo() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(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") + .banner(Banner.builder().build()) + .audio(Audio.builder().build()) + .build())) + .build(); + + // when and then + assertThat(updater.update(request)).isEqualTo(BidRequest.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder() + .btype(asList(1, 2)) + .battr(asList(1, 2)) + .build()) + .audio(Audio.builder().battr(asList(5, 6)).build()) + .build())) + .build()); + } + + @Test + public void shouldNotUpdateMissingAudio() { + // given + final RequestUpdater updater = RequestUpdater.create( + BlockedAttributes.builder() + .badv(asList("domain1.com", "domain2.com")) + .bcat(asList("cat1", "cat2")) + .bapp(asList("app1", "app2")) + .btype(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())) + .imp(singletonList(Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .video(Video.builder().build()) + .build())) .build(); // when and then @@ -273,6 +485,7 @@ public void shouldUpdateAllAttributes() { .btype(asList(1, 2)) .battr(asList(1, 2)) .build()) + .video(Video.builder().battr(asList(3, 4)).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..4e59baac6d4 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 @@ -35,36 +35,46 @@ public Map getProperties() { properties.computeIfAbsent("allowed-" + field + "-for-deals", key -> allowedForDeals); properties.computeIfAbsent("action-overrides", key -> actionOverrides != null - ? actionOverrides.toBuilder() - .field(field) - .build() - : null); + ? actionOverrides.toBuilder() + .field(field) + .build() + : null); return properties; } public static AttributeBuilder badvBuilder() { return Attribute.builder() - .field("adomain"); + .field("adomain"); } public static AttributeBuilder bcatBuilder() { return Attribute.builder() - .field("adv-cat"); + .field("adv-cat"); } public static AttributeBuilder bappBuilder() { return Attribute.builder() - .field("app"); + .field("app"); } public static AttributeBuilder btypeBuilder() { return Attribute.builder() - .field("banner-type"); + .field("banner-type"); } - public static AttributeBuilder battrBuilder() { + public static AttributeBuilder bannerBattrBuilder() { return Attribute.builder() - .field("banner-attr"); + .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/core/config/AttributeActionOverrides.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java index 5080c35330f..1dd8403b859 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/config/AttributeActionOverrides.java @@ -36,42 +36,40 @@ public Map getProperties() { public static AttributeActionOverrides blocked(List> blocked) { return AttributeActionOverrides.builder() - .blocked(blocked) - .build(); + .blocked(blocked) + .build(); } public static AttributeActionOverrides blockUnknown(List blockUnknown) { return AttributeActionOverrides.builder() - .blockUnknown(blockUnknown) - .build(); + .blockUnknown(blockUnknown) + .build(); } - public static AttributeActionOverrides blockFlags( - List enforceBlocks, - List blockUnknown) { + public static AttributeActionOverrides blockFlags(List enforceBlocks, + List blockUnknown) { return AttributeActionOverrides.builder() - .enforceBlocks(enforceBlocks) - .blockUnknown(blockUnknown) - .build(); + .enforceBlocks(enforceBlocks) + .blockUnknown(blockUnknown) + .build(); } - public static AttributeActionOverrides response( - List enforceBlocks, - List blockUnknown, - List> allowedForDeals) { + public static AttributeActionOverrides response(List enforceBlocks, + List blockUnknown, + List> allowedForDeals) { return AttributeActionOverrides.builder() - .enforceBlocks(enforceBlocks) - .blockUnknown(blockUnknown) - .allowedForDeals(allowedForDeals) - .build(); + .enforceBlocks(enforceBlocks) + .blockUnknown(blockUnknown) + .allowedForDeals(allowedForDeals) + .build(); } public static AttributeActionOverrides allowedForDeals(List> allowedForDeals) { return AttributeActionOverrides.builder() - .allowedForDeals(allowedForDeals) - .build(); + .allowedForDeals(allowedForDeals) + .build(); } } 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..7cf65af249e 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,15 +9,16 @@ 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.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; @@ -27,8 +28,6 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -36,28 +35,29 @@ 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() + 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; private Ortb2BlockingBidderRequestHook hook; - @Before + @BeforeEach public void setUp() { given(bidderCatalog.bidderInfoByName(anyString())) .willReturn(bidderInfo(OrtbVersion.ORTB_2_5)); @@ -67,10 +67,20 @@ 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"), + null, + true)); // then assertThat(result.succeeded()).isTrue(); @@ -84,7 +94,7 @@ public void shouldReturnResultWithNoActionWhenNoBlockingAttributes() { @Test public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); // when @@ -105,7 +115,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { @Test public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndDebugDisabled() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); // when @@ -257,10 +267,12 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) { null, null, 0, + null, false, false, null, - Ortb.of(false)); + Ortb.of(false), + 0L); } private static BidRequest emptyRequest() { @@ -270,6 +282,6 @@ private static BidRequest emptyRequest() { } private static ObjectNode toObjectNode(ModuleConfig config) { - return mapper.valueToTree(config); + return MAPPER.valueToTree(config); } } 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..9c4d20693a6 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,8 +6,18 @@ 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.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejection; import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; @@ -17,12 +27,6 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -31,17 +35,20 @@ import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.response.BidType; +import java.util.List; import java.util.function.UnaryOperator; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.prebid.server.auction.model.BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED; +@ExtendWith(MockitoExtension.class) public class Ortb2BlockingRawBidderResponseHookTest { - private static final ObjectMapper mapper = new ObjectMapper() + private static final ObjectMapper MAPPER = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.KEBAB_CASE) .setSerializationInclusion(JsonInclude.Include.NON_NULL); @@ -77,7 +84,7 @@ public void shouldReturnResultWithNoActionWhenNoBidsBlocked() { @Test public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); // when @@ -98,7 +105,7 @@ public void shouldReturnResultWithNoActionAndErrorWhenInvalidAccountConfig() { @Test public void shouldReturnResultWithNoActionAndNoErrorWhenInvalidAccountConfigAndDebugDisabled() { // given - final ObjectNode accountConfig = mapper.createObjectNode() + final ObjectNode accountConfig = MAPPER.createObjectNode() .put("attributes", 1); // when @@ -125,15 +132,17 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { .build()) .build())); + final BidderBid bid1 = bid(bid -> bid.id("bidId1")); + final BidderBid bid2 = bid(bid -> bid.id("bidId2").adomain(singletonList("domain1.com"))); + final BidderBid bid3 = bid(bid -> bid.id("bidId2").adomain(singletonList("domain2.com"))); + // when final Future> result = hook.call( - BidderResponsePayloadImpl.of(asList( - bid(), - bid(bid -> bid.adomain(singletonList("domain1.com"))), - bid(bid -> bid.adomain(singletonList("domain2.com"))))), + BidderResponsePayloadImpl.of(asList(bid1, bid2, bid3)), BidderInvocationContextImpl.builder() .bidder("bidder1") .accountConfig(accountConfig) + .auctionContext(AuctionContext.builder().build()) .moduleContext(ModuleContext.create().with( "bidder1", BlockedAttributes.builder().badv(singletonList("domain2.com")).build())) .debugEnabled(true) @@ -150,12 +159,9 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { }); final PayloadUpdate payloadUpdate = invocationResult.payloadUpdate(); - final BidderResponsePayloadImpl payloadToUpdate = BidderResponsePayloadImpl.of(asList( - bid(), - bid(bid -> bid.adomain(singletonList("domain1.com"))), - bid(bid -> bid.adomain(singletonList("domain2.com"))))); + final BidderResponsePayloadImpl payloadToUpdate = BidderResponsePayloadImpl.of(asList(bid1, bid2, bid3)); assertThat(payloadUpdate.apply(payloadToUpdate)).isEqualTo(BidderResponsePayloadImpl.of( - singletonList(bid(bid -> bid.adomain(singletonList("domain1.com")))))); + singletonList(bid2))); assertThat(invocationResult.analyticsTags()).isEqualTo(TagsImpl.of(singletonList(ActivityImpl.of( "enforce-blocking", @@ -163,9 +169,9 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { asList( ResultImpl.of( "success-blocked", - mapper.createObjectNode() - .set("adomain", mapper.createArrayNode()) - .set("attributes", mapper.createArrayNode() + MAPPER.createObjectNode() + .set("adomain", MAPPER.createArrayNode()) + .set("attributes", MAPPER.createArrayNode() .add("badv")), AppliedToImpl.builder() .bidders(singletonList("bidder1")) @@ -180,15 +186,21 @@ public void shouldReturnResultWithPayloadUpdateAndAnalyticsTags() { .build()), ResultImpl.of( "success-blocked", - mapper.createObjectNode() - .set("adomain", mapper.createArrayNode() + MAPPER.createObjectNode() + .set("adomain", MAPPER.createArrayNode() .add("domain2.com")) - .set("attributes", mapper.createArrayNode() + .set("attributes", MAPPER.createArrayNode() .add("badv")), AppliedToImpl.builder() .bidders(singletonList("bidder1")) .impIds(singletonList("impId1")) .build())))))); + + assertThat(invocationResult.rejections()).containsOnly(entry("bidder1", List.of( + BidRejection.of(bid1, + RESPONSE_REJECTED_ADVERTISER_BLOCKED), + BidRejection.of(bid(bid -> bid.id("bidId2").adomain(singletonList("domain2.com"))), + RESPONSE_REJECTED_ADVERTISER_BLOCKED)))); } @Test @@ -310,7 +322,7 @@ public void shouldReturnResultWithUpdateActionAndNoDebugMessageWhenDebugDisabled } private static BidderBid bid() { - return bid(identity()); + return bid(bid -> bid.id("bidId")); } private static BidderBid bid(UnaryOperator bidCustomizer) { @@ -321,6 +333,6 @@ private static BidderBid bid(UnaryOperator bidCustomizer) { } private static ObjectNode toObjectNode(ModuleConfig config) { - return mapper.valueToTree(config); + return MAPPER.valueToTree(config); } } 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..5812885d88d 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,18 @@ 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.execution.Timeout; +import org.prebid.server.execution.timeout.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 +33,34 @@ public class BidderInvocationContextImpl implements BidderInvocationContext { String bidder; - public static BidderInvocationContext of(String bidder, ObjectNode accountConfig, boolean debugEnabled) { + public static BidderInvocationContext of(String bidder, + ObjectNode accountConfig, + boolean debugEnabled) { + + return BidderInvocationContextImpl.builder() + .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder().build()) + .build()) + .accountConfig(accountConfig) + .debugEnabled(debugEnabled) + .build(); + } + + public static BidderInvocationContext of(String bidder, + Map aliases, + ObjectNode accountConfig, + boolean debugEnabled) { + return BidderInvocationContextImpl.builder() .bidder(bidder) + .auctionContext(AuctionContext.builder() + .bidRequest(BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(aliases) + .build())) + .build()) + .build()) .accountConfig(accountConfig) .debugEnabled(debugEnabled) .build(); diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml new file mode 100644 index 00000000000..30fe61d6498 --- /dev/null +++ b/extra/modules/pb-request-correction/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + pb-request-correction + + pb-request-correction + Request correction module + diff --git a/extra/modules/pb-request-correction/src/lombok.config b/extra/modules/pb-request-correction/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-request-correction/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java new file mode 100644 index 00000000000..3daa937c37c --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProvider { + + private final List correctionProducers; + + public RequestCorrectionProvider(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 -> correctionProducer.produce(config)) + .toList(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java new file mode 100644 index 00000000000..44cac23337e --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.config.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class Config { + + boolean enabled; + + @JsonAlias("pbsdkAndroidInstlRemove") + @JsonProperty("pbsdk-android-instl-remove") + boolean interstitialCorrectionEnabled; + + @JsonAlias("pbsdkUaCleanup") + @JsonProperty("pbsdk-ua-cleanup") + boolean userAgentCorrectionEnabled; +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java new file mode 100644 index 00000000000..aaabf5ab005 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; + +public interface Correction { + + BidRequest apply(BidRequest bidRequest); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java new file mode 100644 index 00000000000..a92132656d5 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; + +public interface CorrectionProducer { + + boolean shouldProduce(Config config, BidRequest bidRequest); + + Correction produce(Config config); +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java new file mode 100644 index 00000000000..75d86c511fb --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +public class InterstitialCorrection implements Correction { + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .imp(bidRequest.getImp().stream() + .map(InterstitialCorrection::removeInterstitial) + .toList()) + .build(); + } + + private static Imp removeInterstitial(Imp imp) { + final Integer interstitial = imp.getInstl(); + return interstitial != null && interstitial == 1 + ? imp.toBuilder().instl(null).build() + : imp; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java new file mode 100644 index 00000000000..c9bd1995867 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.List; +import java.util.Optional; + +public class InterstitialCorrectionProducer implements CorrectionProducer { + + private static final InterstitialCorrection CORRECTION_INSTANCE = new InterstitialCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final String ANDROID = "android"; + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 2; + private static final int MAX_VERSION_PATCH = 3; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isInterstitialCorrectionEnabled() + && hasInterstitialToRemove(bidRequest.getImp()) + && isPrebidMobile(app) + && isAndroid(app) + && isApplicableVersion(app); + } + + private static boolean hasInterstitialToRemove(List imps) { + for (Imp imp : imps) { + final Integer interstitial = imp.getInstl(); + if (interstitial != null && interstitial == 1) { + return true; + } + } + + return false; + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isAndroid(App app) { + return StringUtils.containsIgnoreCase(app.getBundle(), ANDROID); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(InterstitialCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java new file mode 100644 index 00000000000..f1b6b40eacc --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; + +import java.util.regex.Pattern; + +public class UserAgentCorrection implements Correction { + + private static final Pattern USER_AGENT_PATTERN = Pattern.compile("PrebidMobile/[0-9][^ ]*"); + + @Override + public BidRequest apply(BidRequest bidRequest) { + return bidRequest.toBuilder() + .device(correctDevice(bidRequest.getDevice())) + .build(); + } + + private static Device correctDevice(Device device) { + return device.toBuilder() + .ua(USER_AGENT_PATTERN.matcher(device.getUa()).replaceAll("")) + .build(); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java new file mode 100644 index 00000000000..c5010553f5a --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java @@ -0,0 +1,74 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UserAgentCorrectionProducer implements CorrectionProducer { + + private static final UserAgentCorrection CORRECTION_INSTANCE = new UserAgentCorrection(); + + private static final String PREBID_MOBILE = "prebid-mobile"; + private static final Pattern USER_AGENT_PATTERN = Pattern.compile(".*PrebidMobile/[0-9]+[^ ]*.*"); + + private static final int MAX_VERSION_MAJOR = 2; + private static final int MAX_VERSION_MINOR = 1; + private static final int MAX_VERSION_PATCH = 6; + + @Override + public boolean shouldProduce(Config config, BidRequest bidRequest) { + final App app = bidRequest.getApp(); + return config.isUserAgentCorrectionEnabled() + && isPrebidMobile(app) + && isApplicableVersion(app) + && isApplicableDevice(bidRequest.getDevice()); + } + + private static boolean isPrebidMobile(App app) { + final String source = Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getSource) + .orElse(null); + + return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE); + } + + private static boolean isApplicableVersion(App app) { + return Optional.ofNullable(app) + .map(App::getExt) + .map(ExtApp::getPrebid) + .map(ExtAppPrebid::getVersion) + .map(UserAgentCorrectionProducer::checkVersion) + .orElse(false); + } + + private static boolean checkVersion(String version) { + return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH); + } + + private static boolean isApplicableDevice(Device device) { + return Optional.ofNullable(device) + .map(Device::getUa) + .filter(StringUtils::isNotEmpty) + .map(USER_AGENT_PATTERN::matcher) + .map(Matcher::matches) + .orElse(false); + } + + @Override + public Correction produce(Config config) { + return CORRECTION_INSTANCE; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java new file mode 100644 index 00000000000..e2a1268c2f7 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +public class VersionUtil { + + private VersionUtil() { + } + + public static boolean isVersionLessThan(String versionAsString, int major, int minor, int patch) { + return compareVersion(versionAsString, major, minor, patch) < 0; + } + + private static int compareVersion(String versionAsString, int major, int minor, int patch) { + final String[] version = versionAsString.split("\\."); + + final int parsedMajor = getAtAsIntOrDefault(version, 0, -1); + final int parsedMinor = getAtAsIntOrDefault(version, 1, 0); + final int parsedPatch = getAtAsIntOrDefault(version, 2, 0); + + int diff = parsedMajor >= 0 ? parsedMajor - major : 1; + diff = diff == 0 ? parsedMinor - minor : diff; + diff = diff == 0 ? parsedPatch - patch : diff; + + return diff; + } + + private static int getAtAsIntOrDefault(String[] array, int index, int defaultValue) { + return array.length > index ? intOrDefault(array[index], defaultValue) : defaultValue; + } + + private static int intOrDefault(String intAsString, int defaultValue) { + try { + final int parsed = Integer.parseInt(intAsString); + return parsed >= 0 ? parsed : defaultValue; + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java new file mode 100644 index 00000000000..ecbd725e42d --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.pb.request.correction.spring.config; + +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial.InterstitialCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent.UserAgentCorrectionProducer; +import org.prebid.server.hooks.modules.pb.request.correction.v1.RequestCorrectionModule; +import org.prebid.server.json.ObjectMapperProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +@ConditionalOnProperty(prefix = "hooks." + RequestCorrectionModule.CODE, name = "enabled", havingValue = "true") +public class RequestCorrectionModuleConfiguration { + + @Bean + InterstitialCorrectionProducer interstitialCorrectionProducer() { + return new InterstitialCorrectionProducer(); + } + + @Bean + UserAgentCorrectionProducer userAgentCorrectionProducer() { + return new UserAgentCorrectionProducer(); + } + + @Bean + RequestCorrectionProvider requestCorrectionProvider(List correctionProducers) { + return new RequestCorrectionProvider(correctionProducers); + } + + @Bean + RequestCorrectionModule requestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider) { + return new RequestCorrectionModule(requestCorrectionProvider, ObjectMapperProvider.mapper()); + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java new file mode 100644 index 00000000000..10d20a3b823 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +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 RequestCorrectionModule implements Module { + + public static final String CODE = "pb-request-correction"; + + private final Collection> hooks; + + public RequestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) { + this.hooks = Collections.singleton( + new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, mapper)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java new file mode 100644 index 00000000000..215e9795110 --- /dev/null +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java @@ -0,0 +1,106 @@ +package org.prebid.server.hooks.modules.pb.request.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.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.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.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; + +import java.util.List; +import java.util.Objects; + +public class RequestCorrectionProcessedAuctionHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "pb-request-correction-processed-auction-request"; + + private final RequestCorrectionProvider requestCorrectionProvider; + private final ObjectMapper mapper; + + public RequestCorrectionProcessedAuctionHook(RequestCorrectionProvider requestCorrectionProvider, + ObjectMapper mapper) { + + this.requestCorrectionProvider = Objects.requireNonNull(requestCorrectionProvider); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AuctionRequestPayload 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 = payload.bidRequest(); + + final List corrections = requestCorrectionProvider.corrections(config, bidRequest); + if (corrections.isEmpty()) { + return noAction(); + } + + final InvocationResult invocationResult = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of( + applyCorrections(initialPayload.bidRequest(), 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 BidRequest applyCorrections(BidRequest bidRequest, List corrections) { + BidRequest result = bidRequest; + for (Correction correction : corrections) { + result = correction.apply(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-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java new file mode 100644 index 00000000000..56856d10c16 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.pb.request.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.request.correction.core.correction.Correction; +import org.prebid.server.hooks.modules.pb.request.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 RequestCorrectionProviderTest { + + @Mock + private CorrectionProducer correctionProducer; + + private RequestCorrectionProvider target; + + @BeforeEach + public void setUp() { + target = new RequestCorrectionProvider(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(any())).willReturn(correction); + + // when + final List corrections = target.corrections(null, null); + + // then + assertThat(corrections).containsExactly(correction); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java new file mode 100644 index 00000000000..3a44b7158e3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionProducerTest { + + private final InterstitialCorrectionProducer target = new InterstitialCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(emptyList()) + .app(App.builder().build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfBundleNotAnAndroid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundle") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", null), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .interstitialCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder() + .bundle("bundleAndroid") + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java new file mode 100644 index 00000000000..490607a7d5e --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InterstitialCorrectionTest { + + private final InterstitialCorrection target = new InterstitialCorrection(); + + @Test + public void applyShouldCorrectInterstitial() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(asList( + Imp.builder().instl(0).build(), + Imp.builder().build(), + Imp.builder().instl(1).build())) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getImp) + .asInstanceOf(InstanceOfAssertFactories.list(Imp.class)) + .extracting(Imp::getInstl) + .containsExactly(0, null, null); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java new file mode 100644 index 00000000000..cb7e3458bef --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java @@ -0,0 +1,125 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +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 org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.proto.openrtb.ext.request.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionProducerTest { + + private final UserAgentCorrectionProducer target = new UserAgentCorrectionProducer(); + + @Test + public void shouldProduceReturnsFalseIfCorrectionDisabled() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(false) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfThereIsNothingToDo() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().instl(1).build())) + .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfVersionInvalid() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsFalseIfDeviceUserAgentDoesNotMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isFalse(); + } + + @Test + public void shouldProduceReturnsTrueWhenAllConditionsMatch() { + // given + final Config config = Config.builder() + .userAgentCorrectionEnabled(true) + .build(); + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("Blah PrebidMobile/1asdf blah").build()) + .app(App.builder() + .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null)) + .build()) + .build(); + + // when + final boolean result = target.shouldProduce(config, bidRequest); + + // then + assertThat(result).isTrue(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java new file mode 100644 index 00000000000..c8ed5f6762d --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserAgentCorrectionTest { + + private final UserAgentCorrection target = new UserAgentCorrection(); + + @Test + public void applyShouldCorrectUserAgent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().ua("blah PrebidMobile/1asdf blah").build()) + .build(); + + // when + final BidRequest result = target.apply(bidRequest); + + // then + assertThat(result) + .extracting(BidRequest::getDevice) + .extracting(Device::getUa) + .isEqualTo("blah blah"); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java new file mode 100644 index 00000000000..8da1ec6a3c3 --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java @@ -0,0 +1,52 @@ +package org.prebid.server.hooks.modules.pb.request.correction.core.util; + +import org.junit.jupiter.api.Test; + +import static java.lang.Integer.MAX_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +public class VersionUtilTest { + + @Test + public void isVersionLessThanShouldReturnFalseIfVersionGreaterThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 2, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnFalseIfVersionIsEqualToRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 4, 3)).isFalse(); + } + + @Test + public void isVersionLessThenShouldReturnTrueIfVersionIsLessThanRequired() { + // when and then + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 4, 3)).isTrue(); + } + + @Test + public void isVersionLessThenShouldReturnExpectedResults() { + // major + assertThat(VersionUtil.isVersionLessThan("0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("3", 2, 2, 3)).isFalse(); + + // minor + assertThat(VersionUtil.isVersionLessThan("0." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.0", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.3", 2, 2, 3)).isFalse(); + + // patch + assertThat(VersionUtil.isVersionLessThan("0.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("1.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.1." + MAX_VALUE, 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.1", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.2", 2, 2, 3)).isTrue(); + assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 2, 3)).isFalse(); + } +} diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java new file mode 100644 index 00000000000..9250e188cce --- /dev/null +++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java @@ -0,0 +1,120 @@ +package org.prebid.server.hooks.modules.pb.request.correction.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; +import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; +import org.prebid.server.hooks.modules.pb.request.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.auction.AuctionRequestPayload; +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 RequestCorrectionProcessedAuctionHookTest { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + + @Mock + private RequestCorrectionProvider requestCorrectionProvider; + + private RequestCorrectionProcessedAuctionHook target; + + @Mock + private AuctionRequestPayload payload; + + @Mock + private AuctionInvocationContext invocationContext; + + @BeforeEach + public void setUp() { + given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder() + .enabled(true) + .interstitialCorrectionEnabled(true) + .build())); + + target = new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, 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.builder() + .enabled(false) + .interstitialCorrectionEnabled(true) + .build())); + + // 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(requestCorrectionProvider.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(requestCorrectionProvider.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-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml new file mode 100644 index 00000000000..faf24c6baba --- /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.39.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..133a358dff7 --- /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.ResponseCorrectionProvider; +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.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..11c740e9771 --- /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,109 @@ +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.InvocationResultImpl; +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.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/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..9a628f90029 --- /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 static 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..05e3d162083 --- /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,196 @@ +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); + } + + @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); + } + + @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); + } + + @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); + } + + private static Config givenConfig() { + return Config.of(true, AppVideoHtmlConfig.of(true, null)); + } + + private static Config givenConfig(List excludedBidders) { + return Config.of(true, AppVideoHtmlConfig.of(true, excludedBidders)); + } + + 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"); + } +} 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..1379d75b23d --- /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,122 @@ +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..e8db972c01e 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.39.0-SNAPSHOT pb-richmedia-filter diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java index bce3668c0a5..fe3a8a37ac6 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java @@ -20,8 +20,8 @@ public class PbRichmediaFilterModuleConfiguration { @Bean PbRichmediaFilterModule pbRichmediaFilterModule( - @Value("${hooks.modules.pb-richmedia-filter.filter-mraid}") Boolean filterMraid, - @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern}") String mraidScriptPattern) { + @Value("${hooks.modules.pb-richmedia-filter.filter-mraid:false}") boolean filterMraid, + @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern:#{null}}") String mraidScriptPattern) { final ObjectMapper mapper = ObjectMapperProvider.mapper(); final PbRichMediaFilterProperties globalProperties = PbRichMediaFilterProperties.of( 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..6cca35cdf7b 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,7 @@ 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.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -14,8 +15,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,9 +22,11 @@ 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) { - List filteredResponses = new ArrayList<>(); - List analyticsResults = new ArrayList<>(); + public MraidFilterResult filterByPattern(String mraidScriptPattern, + List responses) { + + final List filteredResponses = new ArrayList<>(); + final List analyticsResults = new ArrayList<>(); for (BidderResponse bidderResponse : responses) { final BidderSeatBid seatBid = bidderResponse.getSeatBid(); @@ -43,18 +44,20 @@ 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/core/ModuleConfigResolver.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolver.java index 97a39633d7c..a2320077452 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolver.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/ModuleConfigResolver.java @@ -18,7 +18,6 @@ public ModuleConfigResolver(ObjectMapper mapper, this.globalProperties = Objects.requireNonNull(globalProperties); } - public PbRichMediaFilterProperties resolve(ObjectNode accountConfigNode) { return readAccountConfig(accountConfigNode).orElse(globalProperties); } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/AnalyticsResult.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/AnalyticsResult.java index 79eb7760d36..7a4c390a639 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/AnalyticsResult.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/model/AnalyticsResult.java @@ -1,6 +1,8 @@ package org.prebid.server.hooks.modules.pb.richmedia.filter.model; import lombok.Value; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.model.BidderBid; import java.util.List; import java.util.Map; @@ -15,4 +17,8 @@ public class AnalyticsResult { String bidder; List impId; + + List rejectedBids; + + BidRejectionReason rejectionReason; } 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..dcec0dfa3ff 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 @@ -6,17 +6,19 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.auction.model.BidRejection; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -25,11 +27,13 @@ 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 org.prebid.server.util.ListUtil; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; public class PbRichmediaFilterAllProcessedBidResponsesHook implements AllProcessedBidResponsesHook { @@ -58,25 +62,40 @@ 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); final InvocationAction action = filterResult.hasRejectedBids() ? InvocationAction.update : InvocationAction.no_action; return Future.succeededFuture(toInvocationResult( filterResult.getFilterResult(), toAnalyticsTags(filterResult.getAnalyticsResult()), + toRejections(filterResult.getAnalyticsResult()), action)); } return Future.succeededFuture(toInvocationResult( responses, toAnalyticsTags(Collections.emptyList()), + null, InvocationAction.no_action)); } + private Map> toRejections(List analyticsResults) { + return analyticsResults.stream().collect(Collectors.toMap( + AnalyticsResult::getBidder, + result -> result.getRejectedBids().stream() + .map(bid -> BidRejection.of(bid, result.getRejectionReason())) + .map(Rejection.class::cast) + .toList(), + ListUtil::union)); + } + private static InvocationResult toInvocationResult( List bidderResponses, Tags analyticsTags, + Map> rejections, InvocationAction action) { return InvocationResultImpl.builder() @@ -84,6 +103,7 @@ private static InvocationResult toInvocationRes .action(action) .analyticsTags(analyticsTags) .payloadUpdate(payload -> AllProcessedBidResponsesPayloadImpl.of(bidderResponses)) + .rejections(rejections) .build(); } diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java deleted file mode 100644 index b77fc98a68b..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.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 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-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index bb9e887ca02..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 24f793287b5..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java deleted file mode 100644 index e15359f5c14..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java deleted file mode 100644 index b996bcb4355..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} 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..02add5a0aad 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,8 @@ 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.BidderResponse; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -27,7 +27,8 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() { final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid("imp_id", "adm2"))); // when - final MraidFilterResult filterResult = target.filterByPattern("mraid.js", List.of(responseA, responseB)); + final MraidFilterResult filterResult = target.filterByPattern( + "mraid.js", List.of(responseA, responseB)); // then assertThat(filterResult.getFilterResult()).containsExactly(responseA, responseB); @@ -38,15 +39,14 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() { @Test public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() { // given - final BidderResponse responseA = givenBidderResponse("bidderA", List.of( - givenBid("imp_id1", "adm1"), - givenBid("imp_id2", "adm2"))); - final BidderResponse responseB = givenBidderResponse("bidderB", List.of( - givenBid("imp_id1", "adm1"), - givenBid("imp_id2", "adm2_mraid.js"))); - final BidderResponse responseC = givenBidderResponse("bidderC", List.of( - givenBid("imp_id1", "adm1_mraid.js"), - givenBid("imp_id2", "adm2_mraid.js"))); + final BidderBid givenBid1 = givenBid("imp_id1", "adm1"); + final BidderBid givenBid2 = givenBid("imp_id2", "adm2"); + final BidderBid givenInvalidBid1 = givenBid("imp_id1", "adm1_mraid.js"); + final BidderBid givenInvalidBid2 = givenBid("imp_id2", "adm2_mraid.js"); + + final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid1, givenBid2)); + final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid1, givenInvalidBid2)); + final BidderResponse responseC = givenBidderResponse("bidderC", List.of(givenInvalidBid1, givenInvalidBid2)); // when final MraidFilterResult filterResult = target.filterByPattern( @@ -56,10 +56,10 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() // then final BidderResponse expectedResponseA = givenBidderResponse( "bidderA", - List.of(givenBid("imp_id1", "adm1"), givenBid("imp_id2", "adm2"))); + List.of(givenBid1, givenBid2)); final BidderResponse expectedResponseB = givenBidderResponse( "bidderB", - List.of(givenBid("imp_id1", "adm1")), + List.of(givenBid1), List.of(givenError("imp_id2"))); final BidderResponse expectedResponseC = givenBidderResponse( "bidderC", @@ -70,12 +70,16 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() "success-block", Map.of("richmedia-format", "mraid"), "bidderB", - List.of("imp_id2")); + List.of("imp_id2"), + List.of(givenInvalidBid2), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); final AnalyticsResult expectedAnalyticsResultC = AnalyticsResult.of( "success-block", Map.of("richmedia-format", "mraid"), "bidderC", - List.of("imp_id1", "imp_id2")); + List.of("imp_id1", "imp_id2"), + List.of(givenInvalidBid1, givenInvalidBid2), + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE); assertThat(filterResult.getFilterResult()) .containsExactly(expectedResponseA, expectedResponseB, expectedResponseC); @@ -97,7 +101,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..8dbb33aa35e 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; @@ -19,10 +19,9 @@ public class ModuleConfigResolverTest { private static final PbRichMediaFilterProperties ACCOUNT_PROPERTIES = PbRichMediaFilterProperties.of(true, ""); - 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..b5707a9fe22 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 @@ -1,25 +1,27 @@ package org.prebid.server.hooks.modules.pb.richmedia.filter.v1; import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.response.Bid; 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.BidderResponse; +import org.prebid.server.auction.model.BidRejection; +import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -31,39 +33,42 @@ import java.util.List; import java.util.Map; import java.util.stream.IntStream; +import java.util.stream.Stream; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; 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; +import static org.prebid.server.auction.model.BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE; +@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 + @BeforeEach public void setUp() { - target = new PbRichmediaFilterAllProcessedBidResponsesHook(ObjectMapperProvider.mapper(), mraidFilter, configResolver); + target = new PbRichmediaFilterAllProcessedBidResponsesHook( + ObjectMapperProvider.mapper(), mraidFilter, configResolver); when(configResolver.resolve(any())).thenReturn(PbRichMediaFilterProperties.of(true, "pattern")); } @@ -94,6 +99,7 @@ public void callShouldReturnResultWithNoActionWhenFilterMraidIsFalse() { assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.action()).isEqualTo(InvocationAction.no_action); assertThat(result.analyticsTags()).isNull(); + assertThat(result.rejections()).isNull(); assertThat(result.payloadUpdate().apply(AllProcessedBidResponsesPayloadImpl.of(List.of())).bidResponses()) .isEqualTo(givenResponses); @@ -214,6 +220,24 @@ public void callShouldReturnAnalyticsResultsOfRejectedBids() { "reject-richmedia", "success", List.of(expectedResult1, expectedResult2))))); + + assertThat(result.rejections()).containsOnly( + entry("bidderA", List.of( + BidRejection.of( + BidderBid.builder() + .bid(Bid.builder().id("bid-imp_id1").impid("imp_id1").build()) + .build(), + RESPONSE_REJECTED_INVALID_CREATIVE), + BidRejection.of( + BidderBid.builder() + .bid(Bid.builder().id("bid-imp_id2").impid("imp_id2").build()) + .build(), + RESPONSE_REJECTED_INVALID_CREATIVE))), + entry("bidderB", List.of( + BidRejection.of(BidderBid.builder() + .bid(Bid.builder().id("bid-imp_id3").impid("imp_id3").build()) + .build(), + RESPONSE_REJECTED_INVALID_CREATIVE)))); } @Test @@ -237,6 +261,7 @@ public void callShouldReturnEmptyAnalyticsResultsWhenThereAreNoRejectedBids() { assertThat(result).isNotNull(); assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.analyticsTags()).isNull(); + assertThat(result.rejections()).isEmpty(); } private static List givenBidderResponses(int number) { @@ -246,7 +271,14 @@ private static List givenBidderResponses(int number) { } private static AnalyticsResult givenAnalyticsResult(String bidder, String... rejectedImpIds) { - return AnalyticsResult.of("status", Map.of("key", "value"), bidder, List.of(rejectedImpIds)); + return AnalyticsResult.of( + "status", + Map.of("key", "value"), + bidder, + List.of(rejectedImpIds), + Stream.of(rejectedImpIds).map(impId -> BidderBid.builder().bid( + Bid.builder().id("bid-" + impId).impid(impId).build()).build()).toList(), + RESPONSE_REJECTED_INVALID_CREATIVE); } } 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/pb-rule-engine/pom.xml b/extra/modules/pb-rule-engine/pom.xml new file mode 100644 index 00000000000..0d6d567a809 --- /dev/null +++ b/extra/modules/pb-rule-engine/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + pb-rule-engine + + pb-rule-engine + Rule engine module + diff --git a/extra/modules/pb-rule-engine/src/lombok.config b/extra/modules/pb-rule-engine/src/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java new file mode 100644 index 00000000000..2caf6ee8b30 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/config/PbRuleEngineModuleConfiguration.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.rule.engine.config; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Vertx; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.AccountConfigParser; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.config.StageConfigParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestStageSpecification; +import org.prebid.server.hooks.modules.rule.engine.v1.PbRuleEngineModule; +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.time.Clock; +import java.util.concurrent.ThreadLocalRandom; +import java.util.random.RandomGenerator; + +@Configuration +@ConditionalOnProperty(prefix = "hooks." + PbRuleEngineModule.CODE, name = "enabled", havingValue = "true") +public class PbRuleEngineModuleConfiguration { + + @Bean + PbRuleEngineModule ruleEngineModule(RuleParser ruleParser, + @Value("${datacenter-region:#{null}}") String datacenter) { + + return new PbRuleEngineModule(ruleParser, datacenter); + } + + @Bean + StageConfigParser processedAuctionRequestStageParser( + BidderCatalog bidderCatalog) { + + final RandomGenerator randomGenerator = () -> ThreadLocalRandom.current().nextLong(); + + return new StageConfigParser<>( + randomGenerator, + Stage.processed_auction_request, + new RequestStageSpecification(ObjectMapperProvider.mapper(), bidderCatalog, randomGenerator), + new RequestConditionalRuleFactory()); + } + + @Bean + AccountConfigParser accountConfigParser( + StageConfigParser processedAuctionRequestStageParser) { + + return new AccountConfigParser(ObjectMapperProvider.mapper(), processedAuctionRequestStageParser); + } + + @Bean + RuleParser ruleParser( + @Value("${hooks.pb-rule-engine.rule-cache.expire-after-minutes}") long cacheExpireAfterMinutes, + @Value("${hooks.pb-rule-engine.rule-cache.max-size}") long cacheMaxSize, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-initial-delay-millis}") long delay, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-max-delay-millis}") long maxDelay, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-factor}") double factor, + @Value("${hooks.pb-rule-engine.rule-parsing.retry-exponential-jitter}") double jitter, + AccountConfigParser accountConfigParser, + Vertx vertx, + Clock clock) { + + return new RuleParser( + cacheExpireAfterMinutes, + cacheMaxSize, + ExponentialBackoffRetryPolicy.of(delay, maxDelay, factor, jitter), + accountConfigParser, + vertx, + clock); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java new file mode 100644 index 00000000000..6f542881c66 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParser.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +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 org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; + +import java.util.Objects; + +public class AccountConfigParser { + + private final ObjectMapper mapper; + private final StageConfigParser processedAuctionRequestStageParser; + + public AccountConfigParser( + ObjectMapper mapper, + StageConfigParser processedAuctionRequestStageParser) { + + this.mapper = Objects.requireNonNull(mapper); + this.processedAuctionRequestStageParser = Objects.requireNonNull(processedAuctionRequestStageParser); + } + + public PerStageRule parse(ObjectNode accountConfig) { + final AccountConfig parsedConfig; + try { + parsedConfig = mapper.treeToValue(accountConfig, AccountConfig.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage()); + } + + if (!parsedConfig.isEnabled()) { + return PerStageRule.builder() + .timestamp(parsedConfig.getTimestamp()) + .processedAuctionRequestRule(NoOpRule.create()) + .build(); + } + + return PerStageRule.builder() + .timestamp(parsedConfig.getTimestamp()) + .processedAuctionRequestRule(processedAuctionRequestStageParser.parse(parsedConfig)) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java new file mode 100644 index 00000000000..bd10c138622 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/RuleParser.java @@ -0,0 +1,152 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.execution.retry.RetryPolicy; +import org.prebid.server.execution.retry.Retryable; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +public class RuleParser { + + private static final Logger logger = LoggerFactory.getLogger(RuleParser.class); + + private final AccountConfigParser parser; + private final Vertx vertx; + private final Clock clock; + + private final RetryPolicy retryPolicy; + + private final Map accountIdToParsingAttempt; + private final Map accountIdToRules; + + public RuleParser(long cacheExpireAfterMinutes, + long cacheMaxSize, + RetryPolicy retryPolicy, + AccountConfigParser parser, + Vertx vertx, + Clock clock) { + + this.parser = Objects.requireNonNull(parser); + this.vertx = Objects.requireNonNull(vertx); + this.clock = Objects.requireNonNull(clock); + this.retryPolicy = Objects.requireNonNull(retryPolicy); + + this.accountIdToParsingAttempt = Caffeine.newBuilder() + .expireAfterWrite(cacheExpireAfterMinutes, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build() + .asMap(); + + this.accountIdToRules = Caffeine.newBuilder() + .expireAfterWrite(cacheExpireAfterMinutes, TimeUnit.MINUTES) + .maximumSize(cacheMaxSize) + .build() + .asMap(); + } + + public Future parseForAccount(String accountId, ObjectNode config) { + final PerStageRule cachedRule = accountIdToRules.get(accountId); + + if (cachedRule != null && cachedRule.timestamp().compareTo(getConfigTimestamp(config)) >= 0) { + return Future.succeededFuture(cachedRule); + } + + parseConfig(accountId, config); + return Future.succeededFuture(ObjectUtils.defaultIfNull(cachedRule, PerStageRule.noOp())); + } + + private Instant getConfigTimestamp(ObjectNode config) { + try { + return Optional.of(config) + .map(node -> node.get("timestamp")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText) + .map(Instant::parse) + .orElse(Instant.EPOCH); + } catch (DateTimeParseException exception) { + return Instant.EPOCH; + } + } + + private void parseConfig(String accountId, ObjectNode config) { + final Instant now = clock.instant(); + final ParsingAttempt attempt = accountIdToParsingAttempt.compute( + accountId, (ignored, previousAttempt) -> tryRegisteringNewAttempt(previousAttempt, now)); + + // reference equality used on purpose - if references are equal - then we should parse + if (attempt.timestamp() == now) { + logger.debug("Parsing rule for account {}", accountId); + vertx.executeBlocking(() -> parser.parse(config)) + .onSuccess(result -> succeedParsingAttempt(accountId, result)) + .onFailure(error -> failParsingAttempt(accountId, attempt, error)); + } + } + + private ParsingAttempt tryRegisteringNewAttempt(ParsingAttempt previousAttempt, Instant currentAttemptStart) { + if (previousAttempt == null) { + return new ParsingAttempt.InProgress(currentAttemptStart, retryPolicy); + } + + if (previousAttempt instanceof ParsingAttempt.InProgress) { + return previousAttempt; + } + + if (previousAttempt.retryPolicy() instanceof Retryable previousAttemptRetryPolicy) { + final Instant previouslyDecidedToRetryAfter = previousAttempt.timestamp().plus( + Duration.ofMillis(previousAttemptRetryPolicy.delay())); + + return previouslyDecidedToRetryAfter.isBefore(currentAttemptStart) + ? new ParsingAttempt.InProgress(currentAttemptStart, previousAttemptRetryPolicy.next()) + : previousAttempt; + } + + return previousAttempt; + } + + private void succeedParsingAttempt(String accountId, PerStageRule result) { + accountIdToRules.put(accountId, result); + accountIdToParsingAttempt.remove(accountId); + + logger.debug("Successfully parsed rule-engine config for account {}", accountId); + } + + private void failParsingAttempt(String accountId, ParsingAttempt attempt, Throwable cause) { + accountIdToParsingAttempt.put(accountId, ((ParsingAttempt.InProgress) attempt).failed()); + + logger.error( + "Failed to parse rule-engine config for account %s: %s".formatted(accountId, cause.getMessage()), + cause); + } + + private sealed interface ParsingAttempt { + + Instant timestamp(); + + RetryPolicy retryPolicy(); + + record Failed(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt { + } + + record InProgress(Instant timestamp, RetryPolicy retryPolicy) implements ParsingAttempt { + + public Failed failed() { + return new Failed(timestamp, retryPolicy); + } + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java new file mode 100644 index 00000000000..12c2d9a2720 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParser.java @@ -0,0 +1,175 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountRuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ModelGroupConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ResultFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.RuleSetConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.SchemaFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.AlternativeActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.CompositeRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.DefaultActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RandomWeightedRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidMatcherConfiguration; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTreeFactory; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedEntry; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; +import java.util.random.RandomGenerator; + +public class StageConfigParser { + + private final RandomGenerator randomGenerator; + private final StageSpecification specification; + private final Stage stage; + private final ConditionalRuleFactory conditionalRuleFactory; + + public StageConfigParser(RandomGenerator randomGenerator, + Stage stage, + StageSpecification specification, + ConditionalRuleFactory conditionalRuleFactory) { + + this.randomGenerator = Objects.requireNonNull(randomGenerator); + this.stage = Objects.requireNonNull(stage); + this.specification = Objects.requireNonNull(specification); + this.conditionalRuleFactory = Objects.requireNonNull(conditionalRuleFactory); + } + + public Rule parse(AccountConfig config) { + final List> stageSubrules = config.getRuleSets().stream() + .filter(ruleSet -> stage.equals(ruleSet.getStage())) + .filter(RuleSetConfig::isEnabled) + .map(RuleSetConfig::getModelGroups) + .map(this::parseModelGroupConfigs) + .toList(); + + return stageSubrules.isEmpty() + ? NoOpRule.create() + : CompositeRule.of(stageSubrules); + } + + private Rule parseModelGroupConfigs(List modelGroupConfigs) { + final List>> weightedRules = modelGroupConfigs.stream() + .map(config -> WeightedEntry.of(config.getWeight(), parseModelGroupConfig(config))) + .toList(); + + return RandomWeightedRule.of(randomGenerator, new WeightedList<>(weightedRules)); + } + + private Rule parseModelGroupConfig(ModelGroupConfig config) { + final Rule matchingRule = parseMatchingRule(config); + final Rule defaultRule = parseDefaultActionRule(config); + + return combineRules(matchingRule, defaultRule); + } + + private Rule parseMatchingRule(ModelGroupConfig config) { + final List schemaConfig = config.getSchema(); + final List rulesConfig = config.getRules(); + + if (CollectionUtils.isEmpty(schemaConfig) || CollectionUtils.isEmpty(rulesConfig)) { + return null; + } + + final Schema schema = parseSchema(schemaConfig); + + final List> rules = rulesConfig.stream() + .map(this::parseRuleConfig) + .toList(); + final RuleTree> ruleTree = RuleTreeFactory.buildTree(rules); + + if (schemaConfig.size() != ruleTree.getDepth()) { + throw new InvalidMatcherConfiguration("Schema functions count and rules matchers count mismatch"); + } + + return conditionalRuleFactory.create(schema, ruleTree, config.getAnalyticsKey(), config.getVersion()); + } + + private Schema parseSchema(List schema) { + final List> schemaFunctions = schema.stream() + .map(config -> SchemaFunctionHolder.of( + config.getFunction(), + specification.schemaFunctionByName(config.getFunction()), + config.getArgs())) + .toList(); + + schemaFunctions.forEach(this::validateFunctionConfig); + + return Schema.of(schemaFunctions); + } + + private void validateFunctionConfig(SchemaFunctionHolder holder) { + try { + holder.getSchemaFunction().validateConfig(holder.getConfig()); + } catch (ConfigurationValidationException exception) { + throw new InvalidMatcherConfiguration( + "Function '%s' configuration is invalid: %s".formatted(holder.getName(), exception.getMessage())); + } + } + + private RuleConfig parseRuleConfig(AccountRuleConfig ruleConfig) { + final String ruleFired = String.join("|", ruleConfig.getConditions()); + final List> actions = parseActions(ruleConfig.getResults()); + + return RuleConfig.of(ruleFired, actions); + } + + private Rule parseDefaultActionRule(ModelGroupConfig config) { + final List defaultActionConfig = config.getDefaultAction(); + + if (CollectionUtils.isEmpty(config.getDefaultAction())) { + return null; + } + + return new DefaultActionRule<>( + parseActions(defaultActionConfig), config.getAnalyticsKey(), config.getVersion()); + } + + private List> parseActions(List functionConfigs) { + final List> actions = functionConfigs.stream() + .map(config -> ResultFunctionHolder.of( + config.getFunction(), + specification.resultFunctionByName(config.getFunction()), + config.getArgs())) + .toList(); + + actions.forEach(this::validateActionConfig); + + return actions; + } + + private void validateActionConfig(ResultFunctionHolder action) { + try { + action.getFunction().validateConfig(action.getConfig()); + } catch (ConfigurationValidationException exception) { + throw new InvalidMatcherConfiguration( + "Function '%s' configuration is invalid: %s".formatted(action.getName(), exception.getMessage())); + } + } + + private Rule combineRules(Rule left, Rule right) { + if (left == null && right == null) { + return NoOpRule.create(); + } else if (left != null && right != null) { + return AlternativeActionRule.of(left, right); + } else if (left != null) { + return AlternativeActionRule.of(left, NoOpRule.create()); + } + + return AlternativeActionRule.of(right, NoOpRule.create()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java new file mode 100644 index 00000000000..fab0cd9f208 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountConfig.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +@Value +@Builder +@Jacksonized +public class AccountConfig { + + @Builder.Default + boolean enabled = true; + + @Builder.Default + Instant timestamp = Instant.EPOCH; + + @Builder.Default + @JsonProperty("ruleSets") + List ruleSets = Collections.emptyList(); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java new file mode 100644 index 00000000000..6feb59c8e66 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/AccountRuleConfig.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class AccountRuleConfig { + + List conditions; + + List results; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java new file mode 100644 index 00000000000..f646de8985e --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ModelGroupConfig.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class ModelGroupConfig { + + int weight; + + @JsonProperty("analyticsKey") + String analyticsKey; + + String version; + + List schema; + + @JsonProperty("default") + List defaultAction; + + List rules; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java new file mode 100644 index 00000000000..2b642c633e2 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/ResultFunctionConfig.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionConfig { + + String function; + + ObjectNode args; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java new file mode 100644 index 00000000000..9c54f100977 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/RuleSetConfig.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import org.prebid.server.hooks.execution.model.Stage; + +import java.util.List; + +@Value +@Builder +@Jacksonized +public class RuleSetConfig { + + @Builder.Default + boolean enabled = true; + + Stage stage; + + String name; + + String version; + + @JsonProperty("modelGroups") + List modelGroups; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java new file mode 100644 index 00000000000..264abef359a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/config/model/SchemaFunctionConfig.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config.model; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionConfig { + + String function; + + ObjectNode args; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java new file mode 100644 index 00000000000..e820c787a71 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/Granularity.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +public sealed interface Granularity { + + final class Request implements Granularity { + private static final Request INSTANCE = new Request(); + + private Request() { + } + + public static Request instance() { + return INSTANCE; + } + } + + record Imp(String impId) implements Granularity { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java new file mode 100644 index 00000000000..5b644c8cde5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRule.java @@ -0,0 +1,39 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; + +import java.util.Objects; + +public class PerImpConditionalRule implements Rule { + + private final ConditionalRule delegate; + + public PerImpConditionalRule(ConditionalRule delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + public RuleResult process(BidRequest value, RequestRuleContext context) { + RuleResult result = RuleResult.noAction(value); + for (Imp imp : value.getImp()) { + result = result.mergeWith(delegate.process(result.getValue(), contextForImp(context, imp))); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private RequestRuleContext contextForImp(RequestRuleContext context, Imp imp) { + return RequestRuleContext.of( + context.getAuctionContext(), + new Granularity.Imp(imp.getId()), + context.getDatacenter()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java new file mode 100644 index 00000000000..7958d1e97b9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactory.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +public class RequestConditionalRuleFactory implements ConditionalRuleFactory { + + @Override + public Rule create( + Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion) { + + final ConditionalRule requestMatchingRule = new ConditionalRule<>( + schema, ruleTree, analyticsKey, modelVersion); + + return schema.getFunctions().stream() + .map(SchemaFunctionHolder::getName) + .anyMatch(RequestStageSpecification.PER_IMP_SCHEMA_FUNCTIONS::contains) + + ? new PerImpConditionalRule(requestMatchingRule) + : requestMatchingRule; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java new file mode 100644 index 00000000000..35ea4e1f6b0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestRuleContext.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import lombok.Value; +import org.prebid.server.auction.model.AuctionContext; + +@Value(staticConstructor = "of") +public class RequestRuleContext { + + AuctionContext auctionContext; + + Granularity granularity; + + String datacenter; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java new file mode 100644 index 00000000000..953dd11a74b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestStageSpecification.java @@ -0,0 +1,103 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.ExcludeBiddersFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter.IncludeBiddersFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log.LogATagFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.AdUnitCodeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.BundleInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.ChannelFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DataCenterInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceCountryInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DeviceTypeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.DomainInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.EidInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.FpdAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.GppSidInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.MediaTypeInFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.PrebidKeyFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.TcfInScopeFunction; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.UserFpdAvailableFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidResultFunctionException; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidSchemaFunctionException; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.functions.PercentFunction; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.random.RandomGenerator; + +public class RequestStageSpecification implements StageSpecification { + + public static final Set PER_IMP_SCHEMA_FUNCTIONS = + Set.of(AdUnitCodeFunction.NAME, AdUnitCodeInFunction.NAME, MediaTypeInFunction.NAME); + + private final Map> schemaFunctions; + private final Map> resultFunctions; + + public RequestStageSpecification(ObjectMapper mapper, + BidderCatalog bidderCatalog, + RandomGenerator random) { + + schemaFunctions = new HashMap<>(); + schemaFunctions.put(AdUnitCodeFunction.NAME, new AdUnitCodeFunction()); + schemaFunctions.put(AdUnitCodeInFunction.NAME, new AdUnitCodeInFunction()); + schemaFunctions.put(BundleFunction.NAME, new BundleFunction()); + schemaFunctions.put(BundleInFunction.NAME, new BundleInFunction()); + schemaFunctions.put(ChannelFunction.NAME, new ChannelFunction()); + schemaFunctions.put(DataCenterFunction.NAME, new DataCenterFunction()); + schemaFunctions.put(DataCenterInFunction.NAME, new DataCenterInFunction()); + schemaFunctions.put(DeviceCountryFunction.NAME, new DeviceCountryFunction()); + schemaFunctions.put(DeviceCountryInFunction.NAME, new DeviceCountryInFunction()); + schemaFunctions.put(DeviceTypeFunction.NAME, new DeviceTypeFunction()); + schemaFunctions.put(DeviceTypeInFunction.NAME, new DeviceTypeInFunction()); + schemaFunctions.put(DomainFunction.NAME, new DomainFunction()); + schemaFunctions.put(DomainInFunction.NAME, new DomainInFunction()); + schemaFunctions.put(EidAvailableFunction.NAME, new EidAvailableFunction()); + schemaFunctions.put(EidInFunction.NAME, new EidInFunction()); + schemaFunctions.put(FpdAvailableFunction.NAME, new FpdAvailableFunction()); + schemaFunctions.put(GppSidAvailableFunction.NAME, new GppSidAvailableFunction()); + schemaFunctions.put(GppSidInFunction.NAME, new GppSidInFunction()); + schemaFunctions.put(MediaTypeInFunction.NAME, new MediaTypeInFunction()); + schemaFunctions.put(PercentFunction.NAME, new PercentFunction<>(random)); + schemaFunctions.put(PrebidKeyFunction.NAME, new PrebidKeyFunction()); + schemaFunctions.put(TcfInScopeFunction.NAME, new TcfInScopeFunction()); + schemaFunctions.put(UserFpdAvailableFunction.NAME, new UserFpdAvailableFunction()); + + resultFunctions = Map.of( + IncludeBiddersFunction.NAME, new IncludeBiddersFunction(mapper, bidderCatalog), + ExcludeBiddersFunction.NAME, new ExcludeBiddersFunction(mapper, bidderCatalog), + LogATagFunction.NAME, new LogATagFunction(mapper)); + } + + public SchemaFunction schemaFunctionByName(String name) { + final SchemaFunction function = schemaFunctions.get(name); + if (function == null) { + throw new InvalidSchemaFunctionException(name); + } + + return function; + } + + public ResultFunction resultFunctionByName(String name) { + final ResultFunction function = resultFunctions.get(name); + if (function == null) { + throw new InvalidResultFunctionException(name); + } + + return function; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java new file mode 100644 index 00000000000..0f20384d401 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/AnalyticsMapper.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; + +public class AnalyticsMapper { + + private static final String ACTIVITY_NAME = "pb-rule-engine"; + private static final String SUCCESS_STATUS = "success"; + + private AnalyticsMapper() { + } + + public static Tags toTags(ObjectMapper mapper, + String functionName, + List seatNonBids, + InfrastructureArguments infrastructureArguments, + String analyticsValue) { + + final String analyticsKey = infrastructureArguments.getAnalyticsKey(); + if (StringUtils.isEmpty(analyticsKey)) { + return TagsImpl.of(Collections.emptyList()); + } + + final List removedBidders = seatNonBids.stream() + .map(SeatNonBid::getSeat) + .distinct() + .toList(); + if (CollectionUtils.isEmpty(removedBidders)) { + return TagsImpl.of(Collections.emptyList()); + } + + final List impIds = seatNonBids.stream() + .flatMap(seatNonBid -> seatNonBid.getNonBid().stream()) + .map(NonBid::getImpId) + .distinct() + .toList(); + + final BidRejectionReason reason = seatNonBids.stream() + .flatMap(seatNonBid -> seatNonBid.getNonBid().stream()) + .map(NonBid::getStatusCode) + .findAny() + .orElse(null); + + final AnalyticsData analyticsData = new AnalyticsData( + analyticsKey, + analyticsValue, + infrastructureArguments.getModelVersion(), + infrastructureArguments.getRuleFired(), + functionName, + removedBidders, + reason); + + final Result result = ResultImpl.of( + SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build()); + + return TagsImpl.of(Collections.singletonList( + ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result)))); + } + + private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey, + @JsonProperty("analyticsValue") String analyticsValue, + @JsonProperty("modelVersion") String modelVersion, + @JsonProperty("conditionFired") String conditionFired, + @JsonProperty("resultFunction") String resultFunction, + @JsonProperty("biddersRemoved") List biddersRemoved, + @JsonProperty("seatNonBid") BidRejectionReason seatNonBid) { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java new file mode 100644 index 00000000000..b954abd664a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.util.StreamUtil; + +import java.util.Set; +import java.util.stream.Collectors; + +public class ExcludeBiddersFunction extends FilterBiddersFunction { + + public static final String NAME = "excludeBidders"; + + public ExcludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) { + super(objectMapper, bidderCatalog); + } + + @Override + protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) { + final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt()); + + return StreamUtil.asStream(biddersNode.fieldNames()) + .filter(bidder -> FilterUtils.containsIgnoreCase(bidders.stream(), bidder)) + .filter(bidder -> + ifSyncedId == null || ifSyncedId == isBidderIdSynced(bidder.toLowerCase(), uidsCookie)) + .collect(Collectors.toSet()); + } + + @Override + protected String name() { + return NAME; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java new file mode 100644 index 00000000000..8e2887c4ed8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunction.java @@ -0,0 +1,148 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +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 com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public abstract class FilterBiddersFunction implements ResultFunction { + + private final ObjectMapper mapper; + protected final BidderCatalog bidderCatalog; + + public FilterBiddersFunction(ObjectMapper mapper, BidderCatalog bidderCatalog) { + this.mapper = Objects.requireNonNull(mapper); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public RuleResult apply(ResultFunctionArguments arguments) { + final FilterBiddersFunctionConfig config = parseConfig(arguments.getConfig()); + + final BidRequest bidRequest = arguments.getOperand(); + final InfrastructureArguments infrastructureArguments = + arguments.getInfrastructureArguments(); + + final UidsCookie uidsCookie = infrastructureArguments.getContext().getAuctionContext().getUidsCookie(); + final Boolean ifSyncedId = config.getIfSyncedId(); + final BidRejectionReason rejectionReason = config.getSeatNonBid(); + final Granularity granularity = infrastructureArguments.getContext().getGranularity(); + + final List updatedImps = new ArrayList<>(); + final List seatNonBid = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + if (granularity instanceof Granularity.Imp(String impId) && !StringUtils.equals(impId, imp.getId())) { + updatedImps.add(imp); + continue; + } + + switch (filterBidders(imp, config.getBidders(), ifSyncedId, uidsCookie)) { + case FilterBiddersResult.NoAction noAction -> updatedImps.add(imp); + + case FilterBiddersResult.Reject reject -> + seatNonBid.addAll(toSeatNonBid(imp.getId(), reject.bidders(), rejectionReason)); + + case FilterBiddersResult.Update update -> { + updatedImps.add(update.imp()); + seatNonBid.addAll(toSeatNonBid(imp.getId(), update.bidders(), rejectionReason)); + } + } + } + + final Tags tags = AnalyticsMapper.toTags( + mapper, name(), seatNonBid, infrastructureArguments, config.getAnalyticsValue()); + + if (updatedImps.isEmpty()) { + return RuleResult.rejected(tags, seatNonBid); + } + + final RuleAction action = !seatNonBid.isEmpty() ? RuleAction.UPDATE : RuleAction.NO_ACTION; + final BidRequest result = action == RuleAction.UPDATE + ? bidRequest.toBuilder().imp(updatedImps).build() + : bidRequest; + + return RuleResult.of(result, action, tags, seatNonBid); + } + + private static List toSeatNonBid(String impId, Set bidders, BidRejectionReason reason) { + return bidders.stream() + .map(bidder -> SeatNonBid.of(bidder, Collections.singletonList(NonBid.of(impId, reason)))) + .toList(); + } + + private FilterBiddersResult filterBidders(Imp imp, + Set bidders, + Boolean ifSyncedId, + UidsCookie uidsCookie) { + + final Set biddersToRemove = biddersToRemove(imp, ifSyncedId, bidders, uidsCookie); + if (biddersToRemove.isEmpty()) { + return FilterBiddersResult.NoAction.instance(); + } + + final ObjectNode updatedExt = imp.getExt().deepCopy(); + final ObjectNode updatedBiddersNode = FilterUtils.bidderNode(updatedExt); + biddersToRemove.forEach(updatedBiddersNode::remove); + + return updatedBiddersNode.isEmpty() + ? new FilterBiddersResult.Reject(biddersToRemove) + : new FilterBiddersResult.Update(imp.toBuilder().ext(updatedExt).build(), biddersToRemove); + } + + protected abstract Set biddersToRemove(Imp imp, + Boolean ifSyncedId, + Set bidders, + UidsCookie uidsCookie); + + protected boolean isBidderIdSynced(String bidder, UidsCookie uidsCookie) { + return bidderCatalog.cookieFamilyName(bidder) + .map(uidsCookie::hasLiveUidFrom) + .orElse(false); + } + + @Override + public void validateConfig(ObjectNode config) { + final FilterBiddersFunctionConfig parsedConfig = parseConfig(config); + if (parsedConfig == null) { + throw new ConfigurationValidationException("Configuration is required, but not provided"); + } + + if (CollectionUtils.isEmpty(parsedConfig.getBidders())) { + throw new ConfigurationValidationException("'bidders' field is required"); + } + } + + private FilterBiddersFunctionConfig parseConfig(ObjectNode config) { + try { + return mapper.treeToValue(config, FilterBiddersFunctionConfig.class); + } catch (JsonProcessingException e) { + throw new ConfigurationValidationException(e.getMessage()); + } + } + + protected abstract String name(); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java new file mode 100644 index 00000000000..28ba0f5e518 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersFunctionConfig.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; +import org.prebid.server.auction.model.BidRejectionReason; + +import java.util.Set; + +@Value +@Builder +@Jacksonized +public class FilterBiddersFunctionConfig { + + Set bidders; + + @Builder.Default + @JsonProperty("seatnonbid") + BidRejectionReason seatNonBid = BidRejectionReason.REQUEST_BLOCKED_OPTIMIZED; + + @JsonProperty("ifSyncedId") + Boolean ifSyncedId; + + @JsonProperty("analyticsValue") + String analyticsValue; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java new file mode 100644 index 00000000000..48f71682f84 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterBiddersResult.java @@ -0,0 +1,22 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.iab.openrtb.request.Imp; + +import java.util.Set; + +public sealed interface FilterBiddersResult { + + record NoAction() implements FilterBiddersResult { + private static final NoAction INSTANCE = new NoAction(); + + public static NoAction instance() { + return INSTANCE; + } + } + + record Update(Imp imp, Set bidders) implements FilterBiddersResult { + } + + record Reject(Set bidders) implements FilterBiddersResult { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java new file mode 100644 index 00000000000..d3355c04bd7 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/FilterUtils.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; +import java.util.stream.Stream; + +public class FilterUtils { + + private static final String PREBID = "prebid"; + private static final String BIDDER = "bidder"; + + private FilterUtils() { + } + + public static ObjectNode bidderNode(ObjectNode impExt) { + return Optional.ofNullable(impExt.get(PREBID)) + .filter(JsonNode::isObject) + .map(prebidNode -> (ObjectNode) prebidNode) + .map(prebidNode -> prebidNode.get(BIDDER)) + .filter(JsonNode::isObject) + .map(bidderNode -> (ObjectNode) bidderNode) + .orElseThrow(() -> new IllegalStateException("Impression without ext.prebid.bidder")); + } + + public static boolean containsIgnoreCase(Stream stream, String value) { + return stream.anyMatch(value::equalsIgnoreCase); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java new file mode 100644 index 00000000000..08ea8f2b223 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunction.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.util.StreamUtil; + +import java.util.Set; +import java.util.stream.Collectors; + +public class IncludeBiddersFunction extends FilterBiddersFunction { + + public static final String NAME = "includeBidders"; + + public IncludeBiddersFunction(ObjectMapper objectMapper, BidderCatalog bidderCatalog) { + super(objectMapper, bidderCatalog); + } + + @Override + protected Set biddersToRemove(Imp imp, Boolean ifSyncedId, Set bidders, UidsCookie uidsCookie) { + final ObjectNode biddersNode = FilterUtils.bidderNode(imp.getExt()); + + return StreamUtil.asStream(biddersNode.fieldNames()) + .filter(bidder -> !FilterUtils.containsIgnoreCase(bidders.stream(), bidder) + || (ifSyncedId != null && ifSyncedId != isBidderIdSynced(bidder.toLowerCase(), uidsCookie))) + .collect(Collectors.toSet()); + } + + @Override + protected String name() { + return NAME; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java new file mode 100644 index 00000000000..68c78e65d53 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/AnalyticsMapper.java @@ -0,0 +1,61 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.Collections; +import java.util.List; + +public class AnalyticsMapper { + + private static final String ACTIVITY_NAME = "pb-rule-engine"; + private static final String SUCCESS_STATUS = "success"; + + private AnalyticsMapper() { + } + + public static Tags toTags(ObjectMapper mapper, + InfrastructureArguments infrastructureArguments, + String analyticsValue) { + + final String analyticsKey = infrastructureArguments.getAnalyticsKey(); + if (StringUtils.isEmpty(analyticsKey)) { + return TagsImpl.of(Collections.emptyList()); + } + + final AnalyticsData analyticsData = new AnalyticsData( + analyticsKey, + analyticsValue, + infrastructureArguments.getModelVersion(), + infrastructureArguments.getRuleFired(), + LogATagFunction.NAME); + + final Granularity granularity = infrastructureArguments.getContext().getGranularity(); + final List impIds = granularity instanceof Granularity.Imp(String impId) + ? Collections.singletonList(impId) + : Collections.singletonList("*"); + + final Result result = ResultImpl.of( + SUCCESS_STATUS, mapper.valueToTree(analyticsData), AppliedToImpl.builder().impIds(impIds).build()); + + return TagsImpl.of(Collections.singletonList( + ActivityImpl.of(ACTIVITY_NAME, SUCCESS_STATUS, Collections.singletonList(result)))); + } + + private record AnalyticsData(@JsonProperty("analyticsKey") String analyticsKey, + @JsonProperty("analyticsValue") String analyticsValue, + @JsonProperty("modelVersion") String modelVersion, + @JsonProperty("conditionFired") String conditionFired, + @JsonProperty("resultFunction") String resultFunction) { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java new file mode 100644 index 00000000000..fe7b7543302 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunction.java @@ -0,0 +1,43 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.Collections; +import java.util.Objects; + +public class LogATagFunction implements ResultFunction { + + public static final String NAME = "logAtag"; + + private static final String ANALYTICS_VALUE_FIELD = "analyticsValue"; + + private final ObjectMapper mapper; + + public LogATagFunction(ObjectMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public RuleResult apply(ResultFunctionArguments arguments) { + final Tags tags = AnalyticsMapper.toTags( + mapper, + arguments.getInfrastructureArguments(), + arguments.getConfig().get(ANALYTICS_VALUE_FIELD).asText()); + + return RuleResult.of(arguments.getOperand(), RuleAction.NO_ACTION, tags, Collections.emptyList()); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertString(config, ANALYTICS_VALUE_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java new file mode 100644 index 00000000000..165293f8b71 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunction.java @@ -0,0 +1,35 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class AdUnitCodeFunction implements SchemaFunction { + + public static final String NAME = "adUnitCode"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + return ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .flatMap(AdUnitCodeUtils::extractAdUnitCode) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java new file mode 100644 index 00000000000..6b015863f97 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunction.java @@ -0,0 +1,60 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +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 org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.AdUnitCodeUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class AdUnitCodeInFunction implements SchemaFunction { + + public static final String NAME = "adUnitCodeIn"; + + private static final String CODES_FIELD = "codes"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Critical error in rules engine. Imp id of absent imp supplied")); + + final Set adUnitPotentialCodes = Stream.of( + AdUnitCodeUtils.extractGpid(adUnit), + AdUnitCodeUtils.extractTagId(adUnit), + AdUnitCodeUtils.extractPbAdSlot(adUnit), + AdUnitCodeUtils.extractStoredRequestId(adUnit)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(CODES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(adUnitPotentialCodes::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, CODES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java new file mode 100644 index 00000000000..c99f0b618a0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunction.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class BundleFunction implements SchemaFunction { + + public static final String NAME = "bundle"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getApp()) + .map(App::getBundle) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java new file mode 100644 index 00000000000..9389d686938 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunction.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +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 org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class BundleInFunction implements SchemaFunction { + + public static final String NAME = "bundleIn"; + + private static final String BUNDLES_FIELD = "bundles"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String bundle = Optional.ofNullable(arguments.getOperand().getApp()) + .map(App::getBundle) + .orElse(UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(BUNDLES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(bundle::equals); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, BUNDLES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java new file mode 100644 index 00000000000..2c8a5fd94aa --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunction.java @@ -0,0 +1,38 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +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.ExtRequestPrebidChannel; + +import java.util.Optional; + +public class ChannelFunction implements SchemaFunction { + + public static final String NAME = "channel"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.of(arguments.getOperand()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getChannel) + .map(ExtRequestPrebidChannel::getName) + .map(ChannelFunction::resolveChannel) + .orElse(SchemaFunction.UNDEFINED_RESULT); + } + + private static String resolveChannel(String channel) { + return channel.equals("pbjs") ? "web" : channel; + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java new file mode 100644 index 00000000000..6d3e7acc7c1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunction.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class DataCenterFunction implements SchemaFunction { + + public static final String NAME = "dataCenter"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java new file mode 100644 index 00000000000..8679d9c70e3 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunction.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +public class DataCenterInFunction implements SchemaFunction { + + public static final String NAME = "dataCenterIn"; + + private static final String DATACENTERS_FIELD = "datacenters"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String datacenter = StringUtils.defaultIfEmpty(arguments.getContext().getDatacenter(), UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DATACENTERS_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(datacenter::equalsIgnoreCase); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, DATACENTERS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java new file mode 100644 index 00000000000..f2444f58985 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunction.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class DeviceCountryFunction implements SchemaFunction { + + public static final String NAME = "deviceCountry"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.of(arguments.getOperand()) + .map(BidRequest::getDevice) + .map(Device::getGeo) + .map(Geo::getCountry) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java new file mode 100644 index 00000000000..d2dbf5904ef --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunction.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class DeviceCountryInFunction implements SchemaFunction { + + public static final String NAME = "deviceCountryIn"; + private static final String COUNTRIES_FIELD = "countries"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final String deviceCountry = Optional.of(arguments.getOperand()) + .map(BidRequest::getDevice) + .map(Device::getGeo) + .map(Geo::getCountry) + .orElse(UNDEFINED_RESULT); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(COUNTRIES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(deviceCountry::equalsIgnoreCase); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, COUNTRIES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java new file mode 100644 index 00000000000..490db16a351 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunction.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class DeviceTypeFunction implements SchemaFunction { + + public static final String NAME = "deviceType"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getDevice()) + .map(Device::getDevicetype) + .map(String::valueOf) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java new file mode 100644 index 00000000000..daacb7ad434 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunction.java @@ -0,0 +1,42 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; + +public class DeviceTypeInFunction implements SchemaFunction { + + public static final String NAME = "deviceTypeIn"; + + public static final String TYPES_FIELD = "types"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Integer deviceType = Optional.ofNullable(arguments.getOperand().getDevice()) + .map(Device::getDevicetype) + .orElse(null); + + if (deviceType == null) { + return Boolean.FALSE.toString(); + } + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements()) + .map(JsonNode::asInt) + .anyMatch(deviceType::equals); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfIntegers(config, TYPES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java new file mode 100644 index 00000000000..0fb1e39082f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunction.java @@ -0,0 +1,25 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +public class DomainFunction implements SchemaFunction { + + public static final String NAME = "domain"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return DomainUtils.extractDomain(arguments.getOperand()) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java new file mode 100644 index 00000000000..bfd07a13ea9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunction.java @@ -0,0 +1,50 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util.DomainUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DomainInFunction implements SchemaFunction { + + public static final String NAME = "domainIn"; + + private static final String DOMAINS_FIELD = "domains"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final BidRequest bidRequest = arguments.getOperand(); + + final Set suppliedDomains = Stream.of( + DomainUtils.extractSitePublisherDomain(bidRequest), + DomainUtils.extractAppPublisherDomain(bidRequest), + DomainUtils.extractDoohPublisherDomain(bidRequest), + DomainUtils.extractSiteDomain(bidRequest), + DomainUtils.extractAppDomain(bidRequest), + DomainUtils.extractDoohDomain(bidRequest)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(DOMAINS_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(suppliedDomains::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, DOMAINS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java new file mode 100644 index 00000000000..9c9c8fb2a99 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunction.java @@ -0,0 +1,33 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class EidAvailableFunction implements SchemaFunction { + + public static final String NAME = "eidAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean available = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser) + .map(User::getEids) + .filter(ListUtil::isNotEmpty) + .isPresent(); + + return Boolean.toString(available); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java new file mode 100644 index 00000000000..6c9e4d0070f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunction.java @@ -0,0 +1,46 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class EidInFunction implements SchemaFunction { + + public static final String NAME = "eidIn"; + + private static final String SOURCES_FIELD = "sources"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Set sources = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser) + .map(User::getEids) + .stream() + .flatMap(Collection::stream) + .map(Eid::getSource) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SOURCES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(sources::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, SOURCES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java new file mode 100644 index 00000000000..66c19e14b36 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunction.java @@ -0,0 +1,90 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +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 java.util.Optional; +import java.util.function.Predicate; + +public class FpdAvailableFunction implements SchemaFunction { + + public static final String NAME = "fpdAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final BidRequest bidRequest = arguments.getOperand(); + + final boolean available = isUserDataAvailable(bidRequest) + || isUserExtDataAvailable(bidRequest) + || isSiteContentDataAvailable(bidRequest) + || isSiteExtDataAvailable(bidRequest) + || isAppContentDataAvailable(bidRequest) + || isAppExtDataAvailable(bidRequest); + + return Boolean.toString(available); + } + + private static boolean isUserDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getUser()) + .map(User::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isUserExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ExtUser::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + private static boolean isSiteContentDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getContent) + .map(Content::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isSiteExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getExt) + .map(ExtSite::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + private static boolean isAppContentDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getContent) + .map(Content::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + } + + private static boolean isAppExtDataAvailable(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getExt) + .map(ExtApp::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java new file mode 100644 index 00000000000..ef306e65351 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; + +public class GppSidAvailableFunction implements SchemaFunction { + + public static final String NAME = "gppSidAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean available = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGppSid) + .stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .anyMatch(sid -> sid > 0); + + return Boolean.toString(available); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java new file mode 100644 index 00000000000..46bf0aad29c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunction.java @@ -0,0 +1,44 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.util.StreamUtil; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class GppSidInFunction implements SchemaFunction { + + public static final String NAME = "gppSidIn"; + + private static final String SIDS_FIELD = "sids"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Set sids = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGppSid) + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + final boolean matches = StreamUtil.asStream(arguments.getConfig().get(SIDS_FIELD).elements()) + .map(JsonNode::asInt) + .anyMatch(sids::contains); + + return Boolean.toString(matches); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfIntegers(config, SIDS_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java new file mode 100644 index 00000000000..547efb749ee --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunction.java @@ -0,0 +1,70 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +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 org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.spring.config.bidder.model.MediaType; +import org.prebid.server.util.StreamUtil; + +import java.util.HashSet; +import java.util.Set; + +public class MediaTypeInFunction implements SchemaFunction { + + public static final String NAME = "mediaTypeIn"; + + private static final String TYPES_FIELD = "types"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final RequestRuleContext context = arguments.getContext(); + final String impId = ((Granularity.Imp) context.getGranularity()).impId(); + final BidRequest bidRequest = arguments.getOperand(); + + final Imp adUnit = ListUtils.emptyIfNull(bidRequest.getImp()).stream() + .filter(imp -> StringUtils.equals(imp.getId(), impId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "Critical error in rules engine. Imp id of absent imp supplied")); + + final Set adUnitMediaTypes = adUnitMediaTypes(adUnit); + + final boolean intersects = StreamUtil.asStream(arguments.getConfig().get(TYPES_FIELD).elements()) + .map(JsonNode::asText) + .anyMatch(adUnitMediaTypes::contains); + + return Boolean.toString(intersects); + } + + private static Set adUnitMediaTypes(Imp imp) { + final Set result = new HashSet<>(); + + if (imp.getBanner() != null) { + result.add(MediaType.BANNER.getKey()); + } + if (imp.getVideo() != null) { + result.add(MediaType.VIDEO.getKey()); + } + if (imp.getAudio() != null) { + result.add(MediaType.AUDIO.getKey()); + } + if (imp.getXNative() != null) { + result.add(MediaType.NATIVE.getKey()); + } + + return result; + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertArrayOfStrings(config, TYPES_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java new file mode 100644 index 00000000000..17c42beb5b5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunction.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Optional; + +public class PrebidKeyFunction implements SchemaFunction { + + public static final String NAME = "prebidKey"; + + private static final String KEY_FIELD = "key"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + return Optional.ofNullable(arguments.getOperand().getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getKvps) + .map(kvps -> kvps.get(arguments.getConfig().get(KEY_FIELD).asText())) + .filter(JsonNode::isTextual) + .map(JsonNode::asText) + .orElse(UNDEFINED_RESULT); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertString(config, KEY_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java new file mode 100644 index 00000000000..b98d9672a44 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunction.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.Optional; + +public class TcfInScopeFunction implements SchemaFunction { + + public static final String NAME = "tcfInScope"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final boolean inScope = Optional.of(arguments.getOperand()) + .map(BidRequest::getRegs) + .map(Regs::getGdpr) + .filter(Integer.valueOf(1)::equals) + .isPresent(); + + return Boolean.toString(inScope); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java new file mode 100644 index 00000000000..2a4ecc83206 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunction.java @@ -0,0 +1,41 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ListUtil; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Optional; +import java.util.function.Predicate; + +public class UserFpdAvailableFunction implements SchemaFunction { + + public static final String NAME = "userFpdAvailable"; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final Optional user = Optional.of(arguments.getOperand()) + .map(BidRequest::getUser); + + final boolean userDataAvailable = user.map(User::getData) + .filter(ListUtil::isNotEmpty) + .isPresent(); + + final boolean userExtDataAvailable = user.map(User::getExt) + .map(ExtUser::getData) + .filter(Predicate.not(ObjectNode::isEmpty)) + .isPresent(); + + return Boolean.toString(userDataAvailable || userExtDataAvailable); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertNoArgs(config); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java new file mode 100644 index 00000000000..1d7c8093fa6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/AdUnitCodeUtils.java @@ -0,0 +1,49 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; + +public class AdUnitCodeUtils { + + private AdUnitCodeUtils() { + } + + public static Optional extractAdUnitCode(Imp imp) { + return extractGpid(imp) + .or(() -> extractTagId(imp)) + .or(() -> extractPbAdSlot(imp)) + .or(() -> extractStoredRequestId(imp)); + } + + public static Optional extractGpid(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("gpid")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } + + public static Optional extractTagId(Imp imp) { + return Optional.ofNullable(imp.getTagid()) + .filter(StringUtils::isNotBlank); + } + + public static Optional extractPbAdSlot(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("data")) + .map(data -> data.get("pbadslot")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } + + public static Optional extractStoredRequestId(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("prebid")) + .map(prebid -> prebid.get("storedrequest")) + .map(storedRequest -> storedRequest.get("id")) + .filter(JsonNode::isTextual) + .map(JsonNode::asText); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java new file mode 100644 index 00000000000..b9d57ef8aed --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/util/DomainUtils.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions.util; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; + +import java.util.Optional; + +public class DomainUtils { + + private DomainUtils() { + } + + public static Optional extractDomain(BidRequest bidRequest) { + return extractPublisherDomain(bidRequest) + .or(() -> extractPlainDomain(bidRequest)); + } + + private static Optional extractPublisherDomain(BidRequest bidRequest) { + return extractSitePublisherDomain(bidRequest) + .or(() -> extractAppPublisherDomain(bidRequest)) + .or(() -> extractDoohPublisherDomain(bidRequest)); + } + + public static Optional extractSitePublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()) + .map(Site::getPublisher) + .map(Publisher::getDomain); + } + + public static Optional extractAppPublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()) + .map(App::getPublisher) + .map(Publisher::getDomain); + } + + public static Optional extractDoohPublisherDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getDooh()) + .map(Dooh::getPublisher) + .map(Publisher::getDomain); + } + + private static Optional extractPlainDomain(BidRequest bidRequest) { + return extractSiteDomain(bidRequest) + .or(() -> extractAppDomain(bidRequest)) + .or(() -> extractDoohDomain(bidRequest)); + } + + public static Optional extractSiteDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getSite()).map(Site::getDomain); + } + + public static Optional extractAppDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getApp()).map(App::getDomain); + } + + public static Optional extractDoohDomain(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getDooh()).map(Dooh::getDomain); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java new file mode 100644 index 00000000000..4370ef58502 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRule.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +@Value(staticConstructor = "of") +public class AlternativeActionRule implements Rule { + + Rule delegate; + Rule alternative; + + public RuleResult process(T value, C context) { + try { + return delegate.process(value, context); + } catch (NoMatchingRuleException e) { + return alternative.process(value, context); + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java new file mode 100644 index 00000000000..ca922f1720a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRule.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CompositeRule implements Rule { + + List> subrules; + + @Override + public RuleResult process(T value, C context) { + RuleResult result = RuleResult.noAction(value); + + for (Rule subrule : subrules) { + result = result.mergeWith(subrule.process(value, context)); + + if (result.isReject()) { + return result; + } + } + + return result; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java new file mode 100644 index 00000000000..c56c580d149 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRule.java @@ -0,0 +1,90 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.LookupResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class ConditionalRule implements Rule { + + private final Schema schema; + private final RuleTree> ruleTree; + + private final String modelVersion; + private final String analyticsKey; + + public ConditionalRule(Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion) { + + this.schema = Objects.requireNonNull(schema); + + this.ruleTree = Objects.requireNonNull(ruleTree); + this.analyticsKey = StringUtils.defaultString(analyticsKey); + this.modelVersion = StringUtils.defaultString(modelVersion); + } + + @Override + public RuleResult process(T value, C context) { + final List> schemaFunctions = schema.getFunctions(); + final List matchers = schemaFunctions.stream() + .map(holder -> holder.getSchemaFunction().extract( + SchemaFunctionArguments.of(value, holder.getConfig(), context))) + .map(matcher -> StringUtils.defaultIfEmpty(matcher, SchemaFunction.UNDEFINED_RESULT)) + .toList(); + + final LookupResult> lookupResult = ruleTree.lookup(matchers); + final RuleConfig ruleConfig = lookupResult.getValue(); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(mergeWithSchema(schema, matchers)) + .schemaFunctionMatches(mergeWithSchema(schema, lookupResult.getMatches())) + .ruleFired(ruleConfig.getCondition()) + .analyticsKey(analyticsKey) + .modelVersion(modelVersion) + .build(); + + RuleResult result = RuleResult.noAction(value); + for (ResultFunctionHolder action : ruleConfig.getActions()) { + result = result.mergeWith(applyAction(action, result.getValue(), infrastructureArguments)); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private Map mergeWithSchema(Schema schema, List values) { + return IntStream.range(0, values.size()) + .boxed() + .collect(Collectors.toMap( + idx -> schema.getFunctions().get(idx).getName(), values::get, (left, right) -> left)); + } + + private RuleResult applyAction(ResultFunctionHolder action, + T value, + InfrastructureArguments infrastructureArguments) { + + final ResultFunctionArguments arguments = ResultFunctionArguments.of( + value, action.getConfig(), infrastructureArguments); + + return action.getFunction().apply(arguments); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java new file mode 100644 index 00000000000..918b544e086 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleFactory.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +public interface ConditionalRuleFactory { + + Rule create(Schema schema, + RuleTree> ruleTree, + String analyticsKey, + String modelVersion); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java new file mode 100644 index 00000000000..5e3198b8e24 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRule.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.ListUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@EqualsAndHashCode +public class DefaultActionRule implements Rule { + + private static final String RULE_NAME = "default"; + + private final List> actions; + + private final String analyticsKey; + private final String modelVersion; + + public DefaultActionRule(List> actions, String analyticsKey, String modelVersion) { + this.actions = ListUtils.emptyIfNull(actions); + + this.analyticsKey = Objects.requireNonNull(analyticsKey); + this.modelVersion = Objects.requireNonNull(modelVersion); + } + + @Override + public RuleResult process(T value, C context) { + RuleResult result = RuleResult.noAction(value); + + for (ResultFunctionHolder action : actions) { + result = result.mergeWith(applyAction(action, result.getValue(), context)); + + if (result.isReject()) { + return result; + } + } + + return result; + } + + private RuleResult applyAction(ResultFunctionHolder action, T value, C context) { + final ResultFunctionArguments arguments = ResultFunctionArguments.of( + value, action.getConfig(), infrastructureArguments(context)); + + return action.getFunction().apply(arguments); + } + + private InfrastructureArguments infrastructureArguments(C context) { + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired(RULE_NAME) + .analyticsKey(analyticsKey) + .modelVersion(modelVersion) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java new file mode 100644 index 00000000000..c9ea02dc9f4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/NoOpRule.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; + +@Value(staticConstructor = "create") +public class NoOpRule implements Rule { + + @Override + public RuleResult process(T value, C context) { + return RuleResult.noAction(value); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java new file mode 100644 index 00000000000..c9d3c3383be --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/PerStageRule.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.iab.openrtb.request.BidRequest; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; + +import java.time.Instant; + +@Builder +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class PerStageRule { + + private static final PerStageRule NO_OP = PerStageRule.builder() + .processedAuctionRequestRule(NoOpRule.create()) + .build(); + + Instant timestamp; + + Rule processedAuctionRequestRule; + + public static PerStageRule noOp() { + return NO_OP; + } +} + diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java new file mode 100644 index 00000000000..e3275ef0621 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RandomWeightedRule.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.random.RandomGenerator; + +@Value(staticConstructor = "of") +public class RandomWeightedRule implements Rule { + + RandomGenerator random; + WeightedList> weightedList; + + @Override + public RuleResult process(T value, C context) { + return weightedList.getForSeed(random.nextInt(weightedList.maxSeed())).process(value, context); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java new file mode 100644 index 00000000000..1b369d52d9b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/Rule.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +public interface Rule { + + RuleResult process(T value, C context); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java new file mode 100644 index 00000000000..b123850bebb --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleAction.java @@ -0,0 +1,6 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +public enum RuleAction { + + NO_ACTION, UPDATE, REJECT +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java new file mode 100644 index 00000000000..0c40c198e7c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleConfig.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; + +import java.util.List; + +@Value(staticConstructor = "of") +public class RuleConfig { + + String condition; + + List> actions; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java new file mode 100644 index 00000000000..8967430982b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/RuleResult.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import lombok.Value; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class RuleResult { + + T value; + + RuleAction action; + + Tags analyticsTags; + + List seatNonBid; + + public RuleResult mergeWith(RuleResult other) { + final T value = other.getValue(); + final RuleAction action = merge(this.action, other.getAction()); + final Tags tags = TagsImpl.of(ListUtil.union(analyticsTags.activities(), other.analyticsTags.activities())); + final List seatNonBids = ListUtil.union(seatNonBid, other.seatNonBid); + + return RuleResult.of(value, action, tags, seatNonBids); + } + + private static RuleAction merge(RuleAction left, RuleAction right) { + if (left == RuleAction.REJECT || right == RuleAction.REJECT) { + return RuleAction.REJECT; + } + if (left == RuleAction.UPDATE || right == RuleAction.UPDATE) { + return RuleAction.UPDATE; + } + return RuleAction.NO_ACTION; + } + + public boolean isReject() { + return action == RuleAction.REJECT; + } + + public boolean isUpdate() { + return action == RuleAction.UPDATE; + } + + public static RuleResult noAction(T value) { + return RuleResult.of( + value, RuleAction.NO_ACTION, TagsImpl.of(Collections.emptyList()), Collections.emptyList()); + } + + public static RuleResult rejected(Tags analyticsTags, List seatNonBids) { + return RuleResult.of(null, RuleAction.REJECT, analyticsTags, seatNonBids); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java new file mode 100644 index 00000000000..2083983091f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/StageSpecification.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; + +public interface StageSpecification { + + SchemaFunction schemaFunctionByName(String name); + + ResultFunction resultFunctionByName(String name); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java new file mode 100644 index 00000000000..e7fea8db54d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidMatcherConfiguration.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidMatcherConfiguration extends RuntimeException { + + public InvalidMatcherConfiguration(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java new file mode 100644 index 00000000000..356f7ef53b4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidResultFunctionException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidResultFunctionException extends RuntimeException { + + public InvalidResultFunctionException(String function) { + super("Invalid result function: " + function); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java new file mode 100644 index 00000000000..f1b0f05f7b6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/InvalidSchemaFunctionException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +public class InvalidSchemaFunctionException extends RuntimeException { + + public InvalidSchemaFunctionException(String function) { + super("Invalid schema function: " + function); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java new file mode 100644 index 00000000000..fc1869d3d14 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/exception/NoMatchingRuleException.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.exception; + +import org.prebid.server.hooks.modules.rule.engine.core.util.NoMatchingValueException; + +public class NoMatchingRuleException extends NoMatchingValueException { + + public NoMatchingRuleException() { + super("No matching rule found"); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java new file mode 100644 index 00000000000..f30eb769ddf --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/InfrastructureArguments.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Value +@Builder +public class InfrastructureArguments { + + C context; + + Map schemaFunctionResults; + + Map schemaFunctionMatches; + + String ruleFired; + + String analyticsKey; + + String modelVersion; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java new file mode 100644 index 00000000000..831a85c6af9 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunction.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; + +public interface ResultFunction { + + RuleResult apply(ResultFunctionArguments arguments); + + void validateConfig(ObjectNode config); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java new file mode 100644 index 00000000000..abe28017df8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionArguments.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionArguments { + + T operand; + + ObjectNode config; + + InfrastructureArguments infrastructureArguments; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java new file mode 100644 index 00000000000..5b7476511d4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/result/ResultFunctionHolder.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.result; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ResultFunctionHolder { + + String name; + + ResultFunction function; + + ObjectNode config; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java new file mode 100644 index 00000000000..64ec5aa6e23 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/Schema.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class Schema { + + List> functions; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java new file mode 100644 index 00000000000..887d76eb9ec --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunction.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public interface SchemaFunction { + + String UNDEFINED_RESULT = "undefined"; + + String extract(SchemaFunctionArguments arguments); + + void validateConfig(ObjectNode config); +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java new file mode 100644 index 00000000000..062d1776d0f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionArguments.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionArguments { + + T operand; + + ObjectNode config; + + C context; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java new file mode 100644 index 00000000000..2264cfc2088 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/SchemaFunctionHolder.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SchemaFunctionHolder { + + String name; + + SchemaFunction schemaFunction; + + ObjectNode config; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java new file mode 100644 index 00000000000..2d03ce5e6e0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/schema/functions/PercentFunction.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.schema.functions; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.RequiredArgsConstructor; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ValidationUtils; + +import java.util.random.RandomGenerator; + +@RequiredArgsConstructor +public class PercentFunction implements SchemaFunction { + + public static final String NAME = "percent"; + + private static final String PCT_FIELD = "pct"; + + private final RandomGenerator random; + + @Override + public String extract(SchemaFunctionArguments arguments) { + final int resolvedUpperBound = Math.min(Math.max(arguments.getConfig().get(PCT_FIELD).asInt(), 0), 100); + return Boolean.toString(random.nextInt(100) < resolvedUpperBound); + } + + @Override + public void validateConfig(ObjectNode config) { + ValidationUtils.assertInteger(config, PCT_FIELD); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java new file mode 100644 index 00000000000..283b81a9650 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/LookupResult.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class LookupResult { + + T value; + + List matches; +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java new file mode 100644 index 00000000000..69487fd38b0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleNode.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import java.util.Map; + +public sealed interface RuleNode { + + record IntermediateNode(Map> children) implements RuleNode { + + public RuleNode next(String arg) { + return children.get(arg); + } + } + + record LeafNode(T value) implements RuleNode { + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java new file mode 100644 index 00000000000..6473bd5620f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTree.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import lombok.Getter; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class RuleTree { + + public static final String WILDCARD_MATCHER = "*"; + + private final RuleNode root; + + @Getter + private final int depth; + + public RuleTree(RuleNode root, int depth) { + this.root = Objects.requireNonNull(root); + this.depth = depth; + } + + public LookupResult lookup(List path) { + final List matches = new ArrayList<>(); + RuleNode next = root; + + for (String pathPart : path) { + next = switch (next) { + case RuleNode.IntermediateNode node -> { + final RuleNode result = node.next(pathPart); + matches.add(result == null ? WILDCARD_MATCHER : pathPart); + yield ObjectUtils.defaultIfNull(result, node.next(WILDCARD_MATCHER)); + } + + case RuleNode.LeafNode ignored -> throw new IllegalArgumentException("Argument count mismatch"); + case null -> throw new NoMatchingRuleException(); + }; + } + + return switch (next) { + case RuleNode.LeafNode leaf -> LookupResult.of(leaf.value(), matches); + case RuleNode.IntermediateNode ignored -> throw new IllegalArgumentException("Argument count mismatch"); + case null -> throw new NoMatchingRuleException(); + }; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java new file mode 100644 index 00000000000..83acf0455c6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeFactory.java @@ -0,0 +1,89 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.InvalidMatcherConfiguration; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RuleTreeFactory { + + private RuleTreeFactory() { + } + + public static RuleTree> buildTree(List> rules) { + final List>> parsingContexts = toParsingContexts(rules); + final int depth = getDepth(parsingContexts); + + if (depth == 0) { + throw new InvalidMatcherConfiguration("Rule with no matchers"); + } + + if (!parsingContexts.stream().allMatch(context -> context.argumentMatchers().size() == depth)) { + throw new InvalidMatcherConfiguration("Mismatched arguments count"); + } + + validateRules(parsingContexts); + + return new RuleTree<>(parseRuleNode(parsingContexts), depth); + } + + private static void validateRules(List>> parsingContexts) { + final List ambiguousRules = parsingContexts.stream() + .collect(Collectors.groupingBy(context -> context.value().getCondition(), Collectors.counting())) + .entrySet() + .stream() + .filter(entry -> entry.getValue() > 1) + .map(Map.Entry::getKey) + .toList(); + + if (!ambiguousRules.isEmpty()) { + throw new InvalidMatcherConfiguration("Ambiguous matchers: " + String.join(", ", ambiguousRules)); + } + } + + private static List>> toParsingContexts(List> rules) { + return rules.stream() + .map(rule -> new ParsingContext<>( + List.of(StringUtils.defaultString(rule.getCondition()).split("\\|")), + rule)) + .toList(); + } + + private static int getDepth(List> contexts) { + return contexts.isEmpty() ? 0 : contexts.getFirst().argumentMatchers().size(); + } + + private static RuleNode parseRuleNode(List> parsingContexts) { + if (parsingContexts.size() == 1 && parsingContexts.getFirst().argumentMatchers().isEmpty()) { + return new RuleNode.LeafNode<>(parsingContexts.getFirst().value); + } + + final Map>> subrules = parsingContexts.stream() + .collect(Collectors.groupingBy( + ParsingContext::argumentMatcher, + Collectors.mapping(ParsingContext::next, Collectors.toList()))); + + final Map> parsedSubrules = subrules.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> parseRuleNode(entry.getValue()))); + + return new RuleNode.IntermediateNode<>(parsedSubrules); + } + + private record ParsingContext(List argumentMatchers, T value) { + + public ParsingContext next() { + return new ParsingContext<>(tail(argumentMatchers), value); + } + + public String argumentMatcher() { + return argumentMatchers.getFirst(); + } + } + + private static List tail(List list) { + return list.subList(1, list.size()); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java new file mode 100644 index 00000000000..6c5c194d3e1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ConfigurationValidationException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +public class ConfigurationValidationException extends RuntimeException { + + public ConfigurationValidationException(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java new file mode 100644 index 00000000000..f1dd471b045 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ListUtil.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import java.util.Collection; +import java.util.Objects; + +public class ListUtil { + + private ListUtil() { + } + + public static boolean isNotEmpty(Collection collection) { + return collection != null && !collection.isEmpty() && collection.stream().anyMatch(Objects::nonNull); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java new file mode 100644 index 00000000000..3840cc14ff0 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/NoMatchingValueException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +public class NoMatchingValueException extends RuntimeException { + + public NoMatchingValueException(String message) { + super(message); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java new file mode 100644 index 00000000000..b95663dfea4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/ValidationUtils.java @@ -0,0 +1,69 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.util.StreamUtil; + +import java.util.Optional; +import java.util.function.Predicate; + +public class ValidationUtils { + + private ValidationUtils() { + } + + public static void assertArrayOfStrings(ObjectNode config, String fieldName) { + assertArrayOf( + config, + fieldName, + JsonNode::isTextual, + "Field '%s' is required and has to be an array of strings"); + } + + public static void assertArrayOfIntegers(ObjectNode config, String fieldName) { + assertArrayOf( + config, + fieldName, + JsonNode::isInt, + "Field '%s' is required and has to be an array of integers"); + } + + private static void assertArrayOf(ObjectNode config, + String fieldName, + Predicate predicate, + String messageTemplate) { + + Optional.ofNullable(config) + .map(node -> node.get(fieldName)) + .filter(JsonNode::isArray) + .map(node -> (ArrayNode) node) + .filter(Predicate.not(ArrayNode::isEmpty)) + .filter(node -> StreamUtil.asStream(node.elements()).allMatch(predicate)) + .orElseThrow(() -> new ConfigurationValidationException(messageTemplate.formatted(fieldName))); + } + + public static void assertString(ObjectNode config, String fieldName) { + assertField(config, fieldName, JsonNode::isTextual, "Field '%s' is required and has to be a string"); + } + + public static void assertInteger(ObjectNode config, String fieldName) { + assertField(config, fieldName, JsonNode::isInt, "Field '%s' is required and has to be an integer"); + } + + private static void assertField(ObjectNode config, + String fieldName, + Predicate predicate, + String messageTemplate) { + + if (config == null || !config.has(fieldName) || !predicate.test(config.get(fieldName))) { + throw new ConfigurationValidationException(messageTemplate.formatted(fieldName)); + } + } + + public static void assertNoArgs(ObjectNode config) { + if (config != null && !config.isEmpty()) { + throw new ConfigurationValidationException("No arguments allowed"); + } + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java new file mode 100644 index 00000000000..977a7a6daab --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedEntry.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import lombok.Value; + +@Value +public class WeightedEntry { + + int weight; + + T value; + + private WeightedEntry(int weight, T value) { + this.weight = weight; + this.value = value; + + if (weight <= 0) { + throw new IllegalArgumentException("Weight must be greater than zero"); + } + } + + public static WeightedEntry of(int weight, T value) { + return new WeightedEntry<>(weight, value); + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java new file mode 100644 index 00000000000..92fcd2e455b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/core/util/WeightedList.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.rule.engine.core.util; + +import lombok.EqualsAndHashCode; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +@EqualsAndHashCode +public class WeightedList { + + private final List> entries; + private final int weightSum; + + public WeightedList(List> entries) { + validateEntries(entries); + + this.weightSum = entries.stream().mapToInt(WeightedEntry::getWeight).sum(); + this.entries = prepareEntries(entries); + } + + private void validateEntries(List> entries) { + if (CollectionUtils.isEmpty(entries)) { + throw new IllegalArgumentException("Weighted list cannot be empty"); + } + } + + private List> prepareEntries(List> entries) { + final List> result = new ArrayList<>(entries.size()); + + int cumulativeSum = 0; + + for (WeightedEntry entry : entries) { + cumulativeSum += entry.getWeight(); + result.add(WeightedEntry.of(cumulativeSum, entry.getValue())); + } + + return result; + } + + public T getForSeed(int seed) { + if (seed < 0 || seed >= maxSeed()) { + throw new IllegalArgumentException("Seed number must be between 0 and " + weightSum); + } + + for (WeightedEntry entry : entries) { + if (seed < entry.getWeight()) { + return entry.getValue(); + } + } + + throw new NoMatchingValueException("No entry found for seed " + seed); + } + + public int maxSeed() { + return weightSum; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java new file mode 100644 index 00000000000..7ca7206116f --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineModule.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.rule.engine.v1; + +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +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 PbRuleEngineModule implements Module { + + public static final String CODE = "pb-rule-engine"; + + private final Collection> hooks; + + public PbRuleEngineModule(RuleParser ruleParser, String datacenter) { + this.hooks = Collections.singleton( + new PbRuleEngineProcessedAuctionRequestHook(ruleParser, datacenter)); + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return hooks; + } +} diff --git a/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..2f20d25ffa6 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/main/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHook.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.rule.engine.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +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.ProcessedAuctionRequestHook; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class PbRuleEngineProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "pb-rule-engine-processed-auction-request"; + + private final RuleParser ruleParser; + private final String datacenter; + + public PbRuleEngineProcessedAuctionRequestHook(RuleParser ruleParser, String datacenter) { + this.ruleParser = Objects.requireNonNull(ruleParser); + this.datacenter = Objects.requireNonNull(datacenter); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final AuctionContext context = invocationContext.auctionContext(); + final String accountId = StringUtils.defaultString(invocationContext.auctionContext().getAccount().getId()); + final ObjectNode accountConfig = invocationContext.accountConfig(); + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + + if (accountConfig == null) { + return succeeded(RuleResult.noAction(bidRequest)); + } + + return ruleParser.parseForAccount(accountId, accountConfig) + .map(PerStageRule::processedAuctionRequestRule) + .map(rule -> rule.process( + bidRequest, RequestRuleContext.of(context, Granularity.Request.instance(), datacenter))) + .flatMap(PbRuleEngineProcessedAuctionRequestHook::succeeded) + .recover(PbRuleEngineProcessedAuctionRequestHook::failure); + } + + private static Future> succeeded(RuleResult result) { + final InvocationResultImpl.InvocationResultImplBuilder resultBuilder = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(toInvocationAction(result.getAction())) + .rejections(toRejections(result.getSeatNonBid())) + .analyticsTags(result.getAnalyticsTags()); + + if (result.isUpdate()) { + resultBuilder.payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of(result.getValue())); + } + + return Future.succeededFuture(resultBuilder.build()); + } + + private static InvocationAction toInvocationAction(RuleAction ruleAction) { + return switch (ruleAction) { + case NO_ACTION -> InvocationAction.no_action; + case UPDATE -> InvocationAction.update; + case REJECT -> InvocationAction.reject; + }; + } + + private static List toRejections(SeatNonBid seatNonBid) { + return seatNonBid.getNonBid().stream() + .map(nonBid -> (Rejection) ImpRejection.of(nonBid.getImpId(), nonBid.getStatusCode())) + .toList(); + } + + private static Map> toRejections(List seatNonBids) { + return seatNonBids.stream() + .collect(Collectors.groupingBy(SeatNonBid::getSeat, + Collectors.flatMapping( + seatNonBid -> toRejections(seatNonBid).stream(), + Collectors.toList()))); + } + + private static Future> failure(Throwable error) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.failure) + .action(InvocationAction.no_invocation) + .message(error.getMessage()) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java new file mode 100644 index 00000000000..fe7a33b5a56 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/AccountConfigParserTest.java @@ -0,0 +1,62 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.iab.openrtb.request.BidRequest; +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.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; + +import java.time.Instant; + +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.mock; + +@ExtendWith(MockitoExtension.class) +public class AccountConfigParserTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private AccountConfigParser target; + + @Mock(strictness = LENIENT) + private StageConfigParser processedAuctionRequestStageParser; + + @BeforeEach + public void setUp() { + target = new AccountConfigParser(MAPPER, processedAuctionRequestStageParser); + } + + @Test + public void parseShouldReturnNoOpConfigWhenEnabledIsFalse() { + // when and then + assertThat(target.parse(MAPPER.createObjectNode().set("enabled", BooleanNode.getFalse()))).isEqualTo( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(NoOpRule.create()) + .build()); + } + + @Test + public void parseShouldParseRuleForEachSupportedStage() { + // given + final Rule rule = (Rule) mock(Rule.class); + given(processedAuctionRequestStageParser.parse(any())).willReturn(rule); + + // when and then + assertThat(target.parse(MAPPER.createObjectNode())).isEqualTo( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(rule) + .build()); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java new file mode 100644 index 00000000000..79875a16a0e --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/config/StageConfigParserTest.java @@ -0,0 +1,222 @@ +package org.prebid.server.hooks.modules.rule.engine.core.config; + +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.execution.model.Stage; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.AccountRuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ModelGroupConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.ResultFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.RuleSetConfig; +import org.prebid.server.hooks.modules.rule.engine.core.config.model.SchemaFunctionConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.AlternativeActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.CompositeRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRuleFactory; +import org.prebid.server.hooks.modules.rule.engine.core.rules.DefaultActionRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.NoOpRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RandomWeightedRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.StageSpecification; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTreeFactory; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedEntry; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.random.RandomGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class StageConfigParserTest { + + private StageConfigParser target; + + @Mock(strictness = LENIENT) + private RandomGenerator randomGenerator; + + @Mock(strictness = LENIENT) + private StageSpecification stageSpecification; + + @Mock(strictness = LENIENT) + private ConditionalRuleFactory conditionalRuleFactory; + + @Mock(strictness = LENIENT) + private SchemaFunction schemaFunction; + + @Mock(strictness = LENIENT) + private ResultFunction resultFunction; + + @Mock(strictness = LENIENT) + private Rule matchingRule; + + @Mock(strictness = LENIENT) + private RuleTreeFactory ruleTreeFactory; + + @BeforeEach + public void setUp() { + target = new StageConfigParser<>( + randomGenerator, Stage.processed_auction_request, stageSpecification, conditionalRuleFactory); + } + + @Test + public void parseShouldReturnNoOpRuleWhenSubrulesForStageAreAbsent() { + // when and then + assertThat(target.parse(AccountConfig.builder().build())).isEqualTo(NoOpRule.create()); + } + + @Test + public void parseShouldReturnNoOpRuleWhenEnabledSubrulesForStageAreAbsent() { + // given + final AccountConfig accountConfig = AccountConfig.builder() + .ruleSets(Collections.singletonList( + RuleSetConfig.builder() + .enabled(false) + .stage(Stage.processed_auction_request) + .build())) + .build(); + + // when and then + assertThat(target.parse(accountConfig)).isEqualTo(NoOpRule.create()); + } + + @Test + public void parseShouldCombineModelGroupRulesUnderSameRuleSetIntoRandomWeightedRule() { + // given + final ModelGroupConfig firstModelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey1") + .version("version1") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + final ModelGroupConfig secondModelGroupConfig = ModelGroupConfig.builder() + .weight(2) + .analyticsKey("analyticsKey2") + .version("version2") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + final List modelGroupConfigs = Arrays.asList(firstModelGroupConfig, secondModelGroupConfig); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + given(conditionalRuleFactory.create(any(), any(), any(), any())).willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfigs); + + // when and then + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, + new WeightedList<>(List.of( + WeightedEntry.of(1, AlternativeActionRule.of(matchingRule, NoOpRule.create())), + WeightedEntry.of(2, AlternativeActionRule.of(matchingRule, NoOpRule.create()))))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + @Test + public void parseShouldCombineMatchingRuleWithDefaultUnderSameModelGroup() { + // given + final ModelGroupConfig modelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey") + .version("version") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .defaultAction(List.of(ResultFunctionConfig.of("function3", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + + final ResultFunction secondResultFunction = mock(ResultFunction.class); + given(stageSpecification.resultFunctionByName("function3")).willReturn(secondResultFunction); + + given(conditionalRuleFactory.create(any(), any(), any(), any())).willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfig); + + // when and then + final DefaultActionRule defaultRule = new DefaultActionRule<>( + Collections.singletonList(ResultFunctionHolder.of("function3", secondResultFunction, null)), + "analyticsKey", + "version"); + + final AlternativeActionRule alternativeRule = AlternativeActionRule.of( + matchingRule, defaultRule); + + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, new WeightedList<>(List.of(WeightedEntry.of(1, alternativeRule)))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + @Test + public void parseShouldBuildRuleTreeAndCreateAppropriateMatchingRule() { + // given + final ModelGroupConfig modelGroupConfig = ModelGroupConfig.builder() + .weight(1) + .analyticsKey("analyticsKey") + .version("version") + .schema(List.of(SchemaFunctionConfig.of("function1", null))) + .rules(List.of(AccountRuleConfig.of(List.of("condition1"), + List.of(ResultFunctionConfig.of("function2", null))))) + .build(); + + given(stageSpecification.schemaFunctionByName("function1")).willReturn(schemaFunction); + given(stageSpecification.resultFunctionByName("function2")).willReturn(resultFunction); + + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of("function1", schemaFunction, null))); + + given(conditionalRuleFactory.create(eq(schema), any(), eq("analyticsKey"), eq("version"))) + .willReturn(matchingRule); + + final AccountConfig accountConfig = givenAccountConfig(modelGroupConfig); + + // when and then + final AlternativeActionRule alternativeRule = AlternativeActionRule.of( + matchingRule, NoOpRule.create()); + final RandomWeightedRule weightedRule = RandomWeightedRule.of( + randomGenerator, new WeightedList<>(List.of(WeightedEntry.of(1, alternativeRule)))); + + assertThat(target.parse(accountConfig)).isEqualTo( + CompositeRule.of(Collections.singletonList(weightedRule))); + } + + private static AccountConfig givenAccountConfig(ModelGroupConfig modelGroupConfig) { + return givenAccountConfig(Collections.singletonList(modelGroupConfig)); + } + + private static AccountConfig givenAccountConfig(List modelGroupConfigs) { + return AccountConfig.builder() + .ruleSets(Collections.singletonList( + RuleSetConfig.builder() + .stage(Stage.processed_auction_request) + .modelGroups(modelGroupConfigs) + .build())) + .build(); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java new file mode 100644 index 00000000000..0d50d9c2a16 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/PerImpConditionalRuleTest.java @@ -0,0 +1,95 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +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.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.v1.analytics.Activity; +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.ListUtil; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class PerImpConditionalRuleTest { + + private PerImpConditionalRule target; + + @Mock(strictness = LENIENT) + private ConditionalRule conditionalRule; + + @BeforeEach + public void setUp() { + target = new PerImpConditionalRule(conditionalRule); + } + + @Test + public void processShouldRunMatchingRulePerImpAndCombineResults() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(List.of(Imp.builder().id("1").build(), Imp.builder().id("2").build())) + .build(); + + final RequestRuleContext firstImpContext = RequestRuleContext.of( + AuctionContext.builder().build(), + new Granularity.Imp("1"), + null); + + final BidRequest updatedBidRequest = bidRequest.toBuilder().id("updated").build(); + final List firstActivities = singletonList(ActivityImpl.of("activity1", "success", emptyList())); + final List firstSeatNonBids = singletonList( + SeatNonBid.of("seat1", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))); + given(conditionalRule.process(bidRequest, firstImpContext)).willReturn( + RuleResult.of( + updatedBidRequest, + RuleAction.UPDATE, + TagsImpl.of(firstActivities), + firstSeatNonBids)); + + final RequestRuleContext secondImpContext = RequestRuleContext.of( + AuctionContext.builder().build(), + new Granularity.Imp("2"), + null); + + final BidRequest resultBidRequest = bidRequest.toBuilder().id("updated2").build(); + final List secondActivities = singletonList(ActivityImpl.of("activity2", "success", emptyList())); + final List secondSeatNonBids = singletonList( + SeatNonBid.of("seat2", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + given(conditionalRule.process(updatedBidRequest, secondImpContext)).willReturn( + RuleResult.of( + resultBidRequest, + RuleAction.UPDATE, + TagsImpl.of(secondActivities), + secondSeatNonBids)); + + final RequestRuleContext requestContext = RequestRuleContext.of( + AuctionContext.builder().build(), + Granularity.Request.instance(), + null); + + // when and then + assertThat(target.process(bidRequest, requestContext)).isEqualTo( + RuleResult.of( + resultBidRequest, + RuleAction.UPDATE, + TagsImpl.of(ListUtil.union(firstActivities, secondActivities)), + ListUtil.union(firstSeatNonBids, secondSeatNonBids))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java new file mode 100644 index 00000000000..f0e18b51fae --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/RequestConditionalRuleFactoryTest.java @@ -0,0 +1,63 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request; + +import com.iab.openrtb.request.BidRequest; +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.rule.engine.core.rules.ConditionalRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleConfig; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ThreadLocalRandom; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class RequestConditionalRuleFactoryTest { + + private final RequestConditionalRuleFactory target = new RequestConditionalRuleFactory(); + + @Mock(strictness = LENIENT) + SchemaFunction schemaFunction; + + @Mock(strictness = LENIENT) + RuleTree> ruleTree; + + @Test + void createShouldReturnConditionalRuleWhenSchemaDoesNotContainPerImpFunctions() { + // given + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of("function", schemaFunction, null))); + + // when and then + assertThat(target.create(schema, ruleTree, "key", "version")) + .isInstanceOf(ConditionalRule.class); + } + + @Test + void createShouldReturnPerImpConditionalRuleWhenSchemaContainPerImpFunctions() { + // given + final String perImpSchemaFunctionName = takeRandomElement(RequestStageSpecification.PER_IMP_SCHEMA_FUNCTIONS); + final Schema schema = Schema.of( + Collections.singletonList(SchemaFunctionHolder.of(perImpSchemaFunctionName, schemaFunction, null))); + + // when and then + assertThat(target.create(schema, ruleTree, "key", "version")) + .isInstanceOf(PerImpConditionalRule.class); + } + + private static T takeRandomElement(Collection collection) { + return collection + .stream() + .skip(ThreadLocalRandom.current().nextInt(collection.size())) + .findAny() + .orElse(null); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java new file mode 100644 index 00000000000..18b13c413a5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/ExcludeBiddersFunctionTest.java @@ -0,0 +1,525 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +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.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class ExcludeBiddersFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private ExcludeBiddersFunction target; + + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + @BeforeEach + void setUp() { + target = new ExcludeBiddersFunction(MAPPER, bidderCatalog); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(null)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Configuration is required, but not provided"); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsInvalid() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bidders", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class); + } + + @Test + public void validateConfigShouldThrowErrorWhenBiddersFieldIsEmpty() { + // given + final ObjectNode config = MAPPER.createObjectNode(); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("'bidders' field is required"); + } + + @Test + void applyShouldExcludeBiddersSpecifiedInConfigAndEmitSeatNonBidsWithATags() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder1")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersSpecifiedInConfigOnlyForSpecifiedImpWhenGranularityIsImp() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder3")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder4")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithoutLiveUidSpecifiedInConfigWhenIfSyncedIdSetToFalse() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder1"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(false) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWitLiveUidSpecifiedInConfigWhenIfSyncedIdSetToTrue() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder2"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(true) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldDiscardImpIfAfterUpdateImpExtHasNoBidders() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder3", "bidder4")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3").add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))), + SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + expectedSeatNonBid)); + } + + @Test + void applyShouldRejectBidRequestIfUpdatedRequestHasNoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("excludeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo(RuleResult.rejected(givenATags(expectedActivity), expectedSeatNonBid)); + } + + @Test + void applyShouldNotGenerateATagWhenNoAnalyticsKeySpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .modelVersion("modelVersion") + .build(); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(), + expectedSeatNonBid)); + } + + private static InfrastructureArguments givenInfrastructureArguments( + RequestRuleContext context) { + + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + } + + private static Tags givenATags(Activity... activities) { + return TagsImpl.of(Arrays.asList(activities)); + } + + private AuctionContext givenAuctionContext(String... liveUidBidders) { + final Map uids = Arrays.stream(liveUidBidders) + .collect(Collectors.toMap(Function.identity(), ignored -> UidWithExpiry.live("uid"))); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(uids).build(), new JacksonMapper(MAPPER)); + + Arrays.stream(liveUidBidders).forEach( + bidder -> given(bidderCatalog.cookieFamilyName(bidder)).willReturn(Optional.of(bidder))); + + return AuctionContext.builder() + .uidsCookie(uidsCookie) + .build(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(Arrays.asList(imps)).build(); + } + + private static Imp givenImp(String impId, String... bidders) { + return Imp.builder().id(impId).ext(givenImpExt(bidders)).build(); + } + + private static ObjectNode givenImpExt(String... bidders) { + final ObjectNode biddersNode = MAPPER.createObjectNode(); + final ObjectNode dummyBidderConfigNode = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + Arrays.stream(bidders).forEach(bidder -> biddersNode.set(bidder, dummyBidderConfigNode)); + + return MAPPER.createObjectNode() + .set("prebid", MAPPER.createObjectNode().set("bidder", biddersNode)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java new file mode 100644 index 00000000000..f8cc0aeab1a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/filter/IncludeBiddersFunctionTest.java @@ -0,0 +1,525 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +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.auction.model.BidRejectionReason; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.cookie.UidsCookie; +import org.prebid.server.cookie.model.UidWithExpiry; +import org.prebid.server.cookie.proto.Uids; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +class IncludeBiddersFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private IncludeBiddersFunction target; + + @Mock(strictness = LENIENT) + private BidderCatalog bidderCatalog; + + @BeforeEach + void setUp() { + target = new IncludeBiddersFunction(MAPPER, bidderCatalog); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(null)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Configuration is required, but not provided"); + } + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsInvalid() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bidders", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class); + } + + @Test + public void validateConfigShouldThrowErrorWhenBiddersFieldIsEmpty() { + // given + final ObjectNode config = MAPPER.createObjectNode(); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("'bidders' field is required"); + } + + @Test + void applyShouldExcludeBiddersNotSpecifiedInConfigAndEmitSeatNonBidsWithATags() { + // given + final BidRequest bidRequest = givenBidRequest(givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersNotSpecifiedInConfigOnlyForSpecifiedImpWhenGranularityIsImp() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder3")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithLiveUidOrNotSpecifiedInConfigWhenIfSyncedIdSetToFalse() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder2")) + .ifSyncedId(false) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder1")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder1", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder2")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldExcludeBiddersWithoutLiveUidOrNotSpecifiedInConfigWhenIfSyncedIdSetToTrue() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext("bidder1"), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Collections.singleton("bidder1")) + .ifSyncedId(true) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder2")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final SeatNonBid expectedSeatNonBid = SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(expectedActivity), + Collections.singletonList(expectedSeatNonBid))); + } + + @Test + void applyShouldDiscardImpIfAfterUpdateImpExtHasNoBidders() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2"), + givenImp("impId2", "bidder3", "bidder4")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId2"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1", "bidder2")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder3").add("bidder4")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId2")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder3", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL))), + SeatNonBid.of( + "bidder4", + Collections.singletonList(NonBid.of("impId2", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + final BidRequest expectedBidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + assertThat(result).isEqualTo( + RuleResult.of( + expectedBidRequest, + RuleAction.UPDATE, + givenATags(expectedActivity), + expectedSeatNonBid)); + } + + @Test + void applyShouldRejectBidRequestIfUpdatedRequestHasNoImps() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + givenInfrastructureArguments(context); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final ObjectNode expectedResultValue = MAPPER.createObjectNode(); + expectedResultValue.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedResultValue.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedResultValue.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedResultValue.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedResultValue.set("resultFunction", TextNode.valueOf("includeBidders")); + expectedResultValue.set("biddersRemoved", MAPPER.createArrayNode().add("bidder")); + expectedResultValue.set("seatNonBid", IntNode.valueOf(BidRejectionReason.REQUEST_BLOCKED_GENERAL.getValue())); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder() + .impIds(Collections.singletonList("impId")) + .build(); + + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", + "success", + Collections.singletonList(ResultImpl.of("success", expectedResultValue, expectedAppliedTo))); + + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo(RuleResult.rejected(givenATags(expectedActivity), expectedSeatNonBid)); + } + + @Test + void applyShouldNotGenerateATagWhenNoAnalyticsKeySpecified() { + // given + final BidRequest bidRequest = givenBidRequest( + givenImp("impId", "bidder1", "bidder2")); + + final RequestRuleContext context = RequestRuleContext.of( + givenAuctionContext(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .modelVersion("modelVersion") + .build(); + + final FilterBiddersFunctionConfig config = FilterBiddersFunctionConfig.builder() + .bidders(Set.of("bidder1")) + .seatNonBid(BidRejectionReason.REQUEST_BLOCKED_GENERAL) + .analyticsValue("analyticsValue") + .build(); + + final ResultFunctionArguments arguments = + ResultFunctionArguments.of(bidRequest, MAPPER.valueToTree(config), infrastructureArguments); + + // when + final RuleResult result = target.apply(arguments); + + // then + final List expectedSeatNonBid = List.of( + SeatNonBid.of( + "bidder2", + Collections.singletonList(NonBid.of("impId", BidRejectionReason.REQUEST_BLOCKED_GENERAL)))); + + assertThat(result).isEqualTo( + RuleResult.of( + givenBidRequest(givenImp("impId", "bidder1")), + RuleAction.UPDATE, + givenATags(), + expectedSeatNonBid)); + } + + private static InfrastructureArguments givenInfrastructureArguments( + RequestRuleContext context) { + + return InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + } + + private static Tags givenATags(Activity... activities) { + return TagsImpl.of(Arrays.asList(activities)); + } + + private AuctionContext givenAuctionContext(String... liveUidBidders) { + final Map uids = Arrays.stream(liveUidBidders) + .collect(Collectors.toMap(Function.identity(), ignored -> UidWithExpiry.live("uid"))); + + final UidsCookie uidsCookie = new UidsCookie( + Uids.builder().uids(uids).build(), new JacksonMapper(MAPPER)); + + Arrays.stream(liveUidBidders).forEach( + bidder -> given(bidderCatalog.cookieFamilyName(bidder)).willReturn(Optional.of(bidder))); + + return AuctionContext.builder() + .uidsCookie(uidsCookie) + .build(); + } + + private static BidRequest givenBidRequest(Imp... imps) { + return BidRequest.builder().imp(Arrays.asList(imps)).build(); + } + + private static Imp givenImp(String impId, String... bidders) { + return Imp.builder().id(impId).ext(givenImpExt(bidders)).build(); + } + + private static ObjectNode givenImpExt(String... bidders) { + final ObjectNode biddersNode = MAPPER.createObjectNode(); + final ObjectNode dummyBidderConfigNode = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + Arrays.stream(bidders).forEach(bidder -> biddersNode.set(bidder, dummyBidderConfigNode)); + + return MAPPER.createObjectNode() + .set("prebid", MAPPER.createObjectNode().set("bidder", biddersNode)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java new file mode 100644 index 00000000000..92ac210d950 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/result/functions/log/LogATagFunctionTest.java @@ -0,0 +1,156 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.result.functions.log; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.hooks.v1.analytics.Activity; +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 java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LogATagFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final LogATagFunction target = new LogATagFunction(MAPPER); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenAnalyticsValueFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenAnalyticsValueFieldIsNotAString() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("analyticsValue", MAPPER.createObjectNode()); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'analyticsValue' is required and has to be a string"); + } + + @Test + public void applyShouldEmitATagForRequestAndNotModifyOperand() { + // given + final RequestRuleContext context = RequestRuleContext.of( + AuctionContext.builder().build(), Granularity.Request.instance(), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + + final ObjectNode config = MAPPER.createObjectNode() + .set("analyticsValue", TextNode.valueOf("analyticsValue")); + + final ResultFunctionArguments resultFunctionArguments = + ResultFunctionArguments.of(BidRequest.builder().build(), config, infrastructureArguments); + + // when + final RuleResult result = target.apply(resultFunctionArguments); + + // then + final ObjectNode expectedValues = MAPPER.createObjectNode(); + expectedValues.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedValues.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedValues.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedValues.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedValues.set("resultFunction", TextNode.valueOf("logAtag")); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("*")).build(); + final Result expectedResult = ResultImpl.of("success", expectedValues, expectedAppliedTo); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", "success", Collections.singletonList(expectedResult)); + final Tags expectedTags = TagsImpl.of(Collections.singletonList(expectedActivity)); + + assertThat(result).isEqualTo( + RuleResult.of( + BidRequest.builder().build(), + RuleAction.NO_ACTION, + expectedTags, + Collections.emptyList())); + } + + @Test + public void applyShouldEmitATagForImpAndNotModifyOperand() { + // given + final RequestRuleContext context = RequestRuleContext.of( + AuctionContext.builder().build(), new Granularity.Imp("impId"), null); + + final InfrastructureArguments infrastructureArguments = + InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults(Collections.emptyMap()) + .schemaFunctionMatches(Collections.emptyMap()) + .ruleFired("ruleFired") + .analyticsKey("analyticsKey") + .modelVersion("modelVersion") + .build(); + + final ObjectNode config = MAPPER.createObjectNode() + .set("analyticsValue", TextNode.valueOf("analyticsValue")); + + final ResultFunctionArguments resultFunctionArguments = + ResultFunctionArguments.of(BidRequest.builder().build(), config, infrastructureArguments); + + // when + final RuleResult result = target.apply(resultFunctionArguments); + + // then + final ObjectNode expectedValues = MAPPER.createObjectNode(); + expectedValues.set("analyticsKey", TextNode.valueOf("analyticsKey")); + expectedValues.set("analyticsValue", TextNode.valueOf("analyticsValue")); + expectedValues.set("modelVersion", TextNode.valueOf("modelVersion")); + expectedValues.set("conditionFired", TextNode.valueOf("ruleFired")); + expectedValues.set("resultFunction", TextNode.valueOf("logAtag")); + + final AppliedTo expectedAppliedTo = AppliedToImpl.builder().impIds(Collections.singletonList("impId")).build(); + final Result expectedResult = ResultImpl.of("success", expectedValues, expectedAppliedTo); + final Activity expectedActivity = ActivityImpl.of( + "pb-rule-engine", "success", Collections.singletonList(expectedResult)); + final Tags expectedTags = TagsImpl.of(Collections.singletonList(expectedActivity)); + + assertThat(result).isEqualTo( + RuleResult.of( + BidRequest.builder().build(), + RuleAction.NO_ACTION, + expectedTags, + Collections.emptyList())); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java new file mode 100644 index 00000000000..f129e12eeed --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeFunctionTest.java @@ -0,0 +1,121 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AdUnitCodeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final AdUnitCodeFunction target = new AdUnitCodeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnGpidWhenPresent() { + // given + final Imp imp = Imp.builder() + .id("impId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("gpid"); + } + + @Test + public void extractShouldReturnTagidWhenGpidAbsentAndTagidPresent() { + // given + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("tagId"); + } + + @Test + public void extractShouldReturnPbAdSlotWhenGpidAndTagidAreAbsent() { + // given + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + + final Imp imp = Imp.builder().id("impId").ext(ext).build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("pbadslot"); + } + + @Test + public void extractShouldReturnStoredRequestIdWhenGpidAndTagidAndPbAdSlotAreAbsent() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + + final Imp imp = Imp.builder().id("impId").ext(ext).build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("srid"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenAllAdUnitCodeSourcesAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp("impId"), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java new file mode 100644 index 00000000000..d3705031a51 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/AdUnitCodeInFunctionTest.java @@ -0,0 +1,192 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AdUnitCodeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final AdUnitCodeInFunction target = new AdUnitCodeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("codes", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenCodesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode codesNode = MAPPER.createArrayNode(); + codesNode.add(TextNode.valueOf("test")); + codesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("codes", codesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'codes' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenGpidPresentInConfiguredCodes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "gpid"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenTagidPresentAndGpidIsAbsentInConfiguredCodes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(MAPPER.createObjectNode().put("gpid", "gpid")) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "tagId"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenPbAdSlotPresentAndGpidAndTagidAreAbsentInConfiguredCodes() { + // given + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "pbadslot"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSridPresentAndGpidAndTagidAndPbAdSlotAreAbsentInConfiguredCodes() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "srid"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAdUnitCodesDoesNotMatchConfiguredCodes() { + // given + final ObjectNode prebid = MAPPER.createObjectNode(); + prebid.set("storedrequest", MAPPER.createObjectNode().put("id", "srid")); + final ObjectNode ext = MAPPER.createObjectNode(); + ext.set("prebid", prebid); + ext.set("data", MAPPER.createObjectNode().put("pbadslot", "pbadslot")); + ext.set("gpid", TextNode.valueOf("gpid")); + + final Imp imp = Imp.builder() + .id("impId") + .tagid("tagId") + .ext(ext) + .build(); + + final BidRequest bidRequest = BidRequest.builder().imp(singletonList(imp)).build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "adUnitCode"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... codes) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithCodes(codes), + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp("impId"), "datacenter")); + } + + private ObjectNode givenConfigWithCodes(String... codes) { + final ArrayNode codesNode = MAPPER.createArrayNode(); + Arrays.stream(codes).map(TextNode::valueOf).forEach(codesNode::add); + return MAPPER.createObjectNode().set("codes", codesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java new file mode 100644 index 00000000000..8059c530cc8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +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 org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BundleFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final BundleFunction target = new BundleFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnBundle() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("bundle"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenBundleIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java new file mode 100644 index 00000000000..4a16a45fae8 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/BundleInFunctionTest.java @@ -0,0 +1,112 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +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 org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class BundleInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final BundleInFunction target = new BundleInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("bundles", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenBundlesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode bundlesNode = MAPPER.createArrayNode(); + bundlesNode.add(TextNode.valueOf("test")); + bundlesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("bundles", bundlesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'bundles' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenBundlePresentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "bundle"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenBundleIsAbsentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().bundle("bundle").build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedBundle"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... bundles) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithBundles(bundles), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithBundles(String... bundles) { + final ArrayNode bundlesNode = MAPPER.createArrayNode(); + Arrays.stream(bundles).map(TextNode::valueOf).forEach(bundlesNode::add); + return MAPPER.createObjectNode().set("bundles", bundlesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java new file mode 100644 index 00000000000..c310678e178 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/ChannelFunctionTest.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +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.ExtRequestPrebidChannel; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ChannelFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final ChannelFunction target = new ChannelFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnChannel() { + // given + final ExtRequest ext = ExtRequest.of( + ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("channel")) + .build()); + + final BidRequest bidRequest = BidRequest.builder().ext(ext).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("channel"); + } + + @Test + public void extractShouldReturnWebWhenChannelIsPbjs() { + // given + final ExtRequest ext = ExtRequest.of( + ExtRequestPrebid.builder() + .channel(ExtRequestPrebidChannel.of("pbjs")) + .build()); + + final BidRequest bidRequest = BidRequest.builder().ext(ext).build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("web"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java new file mode 100644 index 00000000000..7f10492234d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DataCenterFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DataCenterFunction target = new DataCenterFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDataCenter() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("datacenter"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenDataCenterIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, null); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String dataCenter) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), dataCenter)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java new file mode 100644 index 00000000000..f5ce0d12a74 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DataCenterInFunctionTest.java @@ -0,0 +1,108 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DataCenterInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DataCenterInFunction target = new DataCenterInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("datacenters", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode datacentersNode = MAPPER.createArrayNode(); + datacentersNode.add(TextNode.valueOf("test")); + datacentersNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("datacenters", datacentersNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'datacenters' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenDataCenterPresentInConfiguredDatacenters() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter", "datacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenBundleIsAbsentInConfiguredBundles() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "datacenter", "expectedDatacenter"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String datacenter, + String... expectedDatacenters) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithDataCenters(expectedDatacenters), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), datacenter)); + } + + private ObjectNode givenConfigWithDataCenters(String... dataCenters) { + final ArrayNode dataCentersNode = MAPPER.createArrayNode(); + Arrays.stream(dataCenters).map(TextNode::valueOf).forEach(dataCentersNode::add); + return MAPPER.createObjectNode().set("datacenters", dataCentersNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java new file mode 100644 index 00000000000..635d4701308 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryFunctionTest.java @@ -0,0 +1,68 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceCountryFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceCountryFunction target = new DeviceCountryFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDeviceCountry() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().geo(Geo.builder().country("country").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("country"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java new file mode 100644 index 00000000000..ef07231b835 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceCountryInFunctionTest.java @@ -0,0 +1,111 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Geo; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceCountryInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceCountryInFunction target = new DeviceCountryInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("countries", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDatacentersFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode countriesNode = MAPPER.createArrayNode(); + countriesNode.add(TextNode.valueOf("test")); + countriesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("countries", countriesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'countries' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenDeviceCountryPresentInConfiguredCountries() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().geo(Geo.builder().country("country").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "country"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenDeviceCountryIsAbsentInConfiguredCountries() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedCountry"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... countries) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithCountries(countries), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithCountries(String... countries) { + final ArrayNode countriesNode = MAPPER.createArrayNode(); + Arrays.stream(countries).map(TextNode::valueOf).forEach(countriesNode::add); + return MAPPER.createObjectNode().set("countries", countriesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java new file mode 100644 index 00000000000..9e4fe40ec75 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceTypeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceTypeFunction target = new DeviceTypeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnDeviceType() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().devicetype(12345).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("12345"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenChannelIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java new file mode 100644 index 00000000000..0bbfc96d6ca --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DeviceTypeInFunctionTest.java @@ -0,0 +1,110 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DeviceTypeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DeviceTypeInFunction target = new DeviceTypeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("types", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArrayOfIntegers() { + // given + final ArrayNode typesNode = MAPPER.createArrayNode(); + typesNode.add(TextNode.valueOf("test")); + typesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("types", typesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of integers"); + } + + @Test + public void extractShouldReturnTrueWhenDeviceTypePresentInConfiguredTypes() { + // given + final BidRequest bidRequest = BidRequest.builder() + .device(Device.builder().devicetype(12345).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 12345); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenDeviceTypeIsAbsentInConfiguredTypes() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + int... types) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithTypes(types), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithTypes(int... types) { + final ArrayNode typesNode = MAPPER.createArrayNode(); + Arrays.stream(types).mapToObj(IntNode::valueOf).forEach(typesNode::add); + return MAPPER.createObjectNode().set("types", typesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java new file mode 100644 index 00000000000..8183ce51dc2 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainFunctionTest.java @@ -0,0 +1,136 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DomainFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DomainFunction target = new DomainFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnSitePublisherDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnAppPublisherDomainWhenSiteHasNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnDoohPublisherDomainWhenSiteAndAppHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .dooh(Dooh.builder().publisher(Publisher.builder().domain("domain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnSiteDomainWhenPublisherHasNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnAppDomainWhenPublisherAndSiteHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .app(App.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldReturnDoohDomainWhenPublisherAndSiteAndAppHaveNoDomain() { + // given + final BidRequest bidRequest = BidRequest.builder() + .dooh(Dooh.builder().domain("domain").build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("domain"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenDomainIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java new file mode 100644 index 00000000000..20be78ab820 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/DomainInFunctionTest.java @@ -0,0 +1,241 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +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.Dooh; +import com.iab.openrtb.request.Publisher; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DomainInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final DomainInFunction target = new DomainInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("domains", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode domainsNode = MAPPER.createArrayNode(); + domainsNode.add(TextNode.valueOf("test")); + domainsNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("domains", domainsNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'domains' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenSitePublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "sitePubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppPublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "appPubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenDoohPublisherDomainIsPresentInConfiguredDomains() { + // given + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().publisher(Publisher.builder().domain("sitePubDomain").build()).build()) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "doohPubDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(App.builder().publisher(Publisher.builder().domain("appPubDomain").build()).build()) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "siteDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(Dooh.builder().publisher(Publisher.builder().domain("doohPubDomain").build()).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "appDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenDoohDomainIsPresentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final Dooh dooh = Dooh.builder() + .publisher(Publisher.builder().domain("doohPubDomain").build()) + .domain("doohDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(dooh) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "doohDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllSuppliedDomainsAreAbsentInConfiguredDomains() { + // given + final Site site = Site.builder() + .publisher(Publisher.builder().domain("sitePubDomain").build()) + .domain("siteDomain") + .build(); + + final App app = App.builder() + .publisher(Publisher.builder().domain("appPubDomain").build()) + .domain("appDomain") + .build(); + + final Dooh dooh = Dooh.builder() + .publisher(Publisher.builder().domain("doohPubDomain").build()) + .domain("doohDomain") + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .app(app) + .dooh(dooh) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedDomain"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... domains) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithDomains(domains), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithDomains(String... domains) { + final ArrayNode domainsNode = MAPPER.createArrayNode(); + Arrays.stream(domains).map(TextNode::valueOf).forEach(domainsNode::add); + return MAPPER.createObjectNode().set("domains", domainsNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java new file mode 100644 index 00000000000..c720f57ff0a --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidAvailableFunctionTest.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EidAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final EidAvailableFunction target = new EidAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenEidsArePresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenEidsAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenEidsListContainsOnlyNulls() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(null)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java new file mode 100644 index 00000000000..f0018bc18f1 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/EidInFunctionTest.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EidInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final EidInFunction target = new EidInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("sources", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenDomainsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode sourcesNode = MAPPER.createArrayNode(); + sourcesNode.add(TextNode.valueOf("test")); + sourcesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("sources", sourcesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sources' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenAnyOfUserEidSourcesPresentInConfiguredSources() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().source("source").build())).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "source"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllUserEidSourcesAbsentInConfiguredSources() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(Eid.builder().source("source").build())).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "expectedSource"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String... sources) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithSources(sources), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithSources(String... sources) { + final ArrayNode sourcesNode = MAPPER.createArrayNode(); + Arrays.stream(sources).map(TextNode::valueOf).forEach(sourcesNode::add); + return MAPPER.createObjectNode().set("sources", sourcesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java new file mode 100644 index 00000000000..dff830c0e2b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/FpdAvailableFunctionTest.java @@ -0,0 +1,160 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.Data; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +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 java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class FpdAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final FpdAvailableFunction target = new FpdAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenUserDataPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenUserExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().ext(ExtUser.builder().data(extData).build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteContentDataPresent() { + // given + final Site site = Site.builder() + .content(Content.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenSiteExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final Site site = Site.builder() + .ext(ExtSite.of(null, extData)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .site(site) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppContentDataPresent() { + // given + final App app = App.builder() + .content(Content.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .app(app) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAppExtDataPresent() { + // given + final ObjectNode extData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final App app = App.builder() + .ext(ExtApp.of(null, extData)) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .app(app) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenNoFpdAvailable() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java new file mode 100644 index 00000000000..6cff68fbe1d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidAvailableFunctionTest.java @@ -0,0 +1,97 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GppSidAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final GppSidAvailableFunction target = new GppSidAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenPositiveGppSidIsPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(1)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidListContainsOnlyNulls() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().eids(Collections.singletonList(null)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + @Test + public void extractShouldReturnFalseWhenGppSidListContainsOnlyNonPositiveValues() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(List.of(-1, 0)).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java new file mode 100644 index 00000000000..25bbcd00b1d --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/GppSidInFunctionTest.java @@ -0,0 +1,116 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class GppSidInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final GppSidInFunction target = new GppSidInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("sids", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void validateConfigShouldThrowErrorWhenSidsFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode sidsNode = MAPPER.createArrayNode(); + sidsNode.add(TextNode.valueOf("test")); + sidsNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("sids", sidsNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'sids' is required and has to be an array of integers"); + } + + @Test + public void extractShouldReturnTrueWhenAnyOfRegsGppSidIsPresentInConfiguredSids() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(1)).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenAllOfRegsGppSidAreAbsentConfiguredSids() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gppSid(Collections.singletonList(2)).build()) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, 1); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + com.iab.openrtb.request.BidRequest bidRequest, + int... sids) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithSids(sids), + RequestRuleContext.of( + AuctionContext.builder().build(), + Granularity.Request.instance(), + "datacenter")); + } + + private ObjectNode givenConfigWithSids(int... sids) { + final ArrayNode sidsNode = MAPPER.createArrayNode(); + Arrays.stream(sids).mapToObj(IntNode::valueOf).forEach(sidsNode::add); + return MAPPER.createObjectNode().set("sids", sidsNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java new file mode 100644 index 00000000000..50482b7e042 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/MediaTypeInFunctionTest.java @@ -0,0 +1,185 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +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.Native; +import com.iab.openrtb.request.Video; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class MediaTypeInFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final MediaTypeInFunction target = new MediaTypeInFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArray() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("types", TextNode.valueOf("test")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void validateConfigShouldThrowErrorWhenTypesFieldIsNotAnArrayOfStrings() { + // given + final ArrayNode typesNode = MAPPER.createArrayNode(); + typesNode.add(TextNode.valueOf("test")); + typesNode.add(IntNode.valueOf(1)); + final ObjectNode config = MAPPER.createObjectNode().set("types", typesNode); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'types' is required and has to be an array of strings"); + } + + @Test + public void extractShouldReturnTrueWhenBannerPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .banner(Banner.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "banner"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenVideoPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .video(Video.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "video"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenAudioPresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .audio(Audio.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "audio"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenNativePresentOnProvidedImpAndConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .xNative(Native.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "native"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenImpMediaTypeIsAbsentInConfiguredTypes() { + // given + final Imp imp = Imp.builder() + .id("impId") + .xNative(Native.builder().build()) + .build(); + + final BidRequest bidRequest = BidRequest.builder() + .imp(Collections.singletonList(imp)) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "impId", "expectedMediaType"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String impId, + String... types) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithTypes(types), + RequestRuleContext.of(AuctionContext.builder().build(), new Granularity.Imp(impId), "datacenter")); + } + + private ObjectNode givenConfigWithTypes(String... types) { + final ArrayNode typesNode = MAPPER.createArrayNode(); + Arrays.stream(types).map(TextNode::valueOf).forEach(typesNode::add); + return MAPPER.createObjectNode().set("types", typesNode); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java new file mode 100644 index 00000000000..7a8f5f4e2c4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/PrebidKeyFunctionTest.java @@ -0,0 +1,93 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class PrebidKeyFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final PrebidKeyFunction target = new PrebidKeyFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenConfigIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenKeyFieldIsAbsent() { + // when and then + assertThatThrownBy(() -> target.validateConfig(MAPPER.createObjectNode())) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void validateConfigShouldThrowErrorWhenKeyFieldIsNotAString() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("key", IntNode.valueOf(1)); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("Field 'key' is required and has to be a string"); + } + + @Test + public void extractShouldReturnExtPrebidKvpsValueBySpecifiedKey() { + // given + final ObjectNode extPrebidKvpsNode = MAPPER.createObjectNode().set("key", TextNode.valueOf("value")); + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().kvps(extPrebidKvpsNode).build())) + .build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "key"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("value"); + } + + @Test + public void extractShouldFallbackToUndefinedWhenExtPrebidKvpsValueBySpecifiedKeyIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = + givenFunctionArguments(bidRequest, "key"); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("undefined"); + } + + private SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest, + String key) { + + return SchemaFunctionArguments.of( + bidRequest, + givenConfigWithKey(key), + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } + + private ObjectNode givenConfigWithKey(String key) { + return MAPPER.createObjectNode().set("key", TextNode.valueOf(key)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java new file mode 100644 index 00000000000..1fd079243f5 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/TcfInScopeFunctionTest.java @@ -0,0 +1,67 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TcfInScopeFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final TcfInScopeFunction target = new TcfInScopeFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenTcfInScope() { + // given + final BidRequest bidRequest = BidRequest.builder() + .regs(Regs.builder().gdpr(1).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenTcfNotInScope() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java new file mode 100644 index 00000000000..c834345f88c --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/request/schema/functions/UserFpdAvailableFunctionTest.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.rule.engine.core.request.schema.functions; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.util.ConfigurationValidationException; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class UserFpdAvailableFunctionTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final UserFpdAvailableFunction target = new UserFpdAvailableFunction(); + + @Test + public void validateConfigShouldThrowErrorWhenArgumentsArePresent() { + // given + final ObjectNode config = MAPPER.createObjectNode().set("args", TextNode.valueOf("args")); + + // when and then + assertThatThrownBy(() -> target.validateConfig(config)) + .isInstanceOf(ConfigurationValidationException.class) + .hasMessage("No arguments allowed"); + } + + @Test + public void extractShouldReturnTrueWhenNonNullUserDataIsPresent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().data(Collections.singletonList(Data.builder().build())).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnTrueWhenUserExtDataIsPresent() { + // given + final ObjectNode extUserData = MAPPER.createObjectNode().set("someData", TextNode.valueOf("someData")); + final BidRequest bidRequest = BidRequest.builder() + .user(User.builder().ext(ExtUser.builder().data(extUserData).build()).build()) + .build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("true"); + } + + @Test + public void extractShouldReturnFalseWhenUserDataAndUserExtDataAreAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + final SchemaFunctionArguments arguments = givenFunctionArguments(bidRequest); + + // when and then + assertThat(target.extract(arguments)).isEqualTo("false"); + } + + private static SchemaFunctionArguments givenFunctionArguments( + BidRequest bidRequest) { + + return SchemaFunctionArguments.of( + bidRequest, + null, + RequestRuleContext.of(AuctionContext.builder().build(), Granularity.Request.instance(), "datacenter")); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java new file mode 100644 index 00000000000..1a7d793dadf --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/AlternativeActionRuleTest.java @@ -0,0 +1,61 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +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.rule.engine.core.rules.exception.NoMatchingRuleException; + +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.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +public class AlternativeActionRuleTest { + + private static final RuleResult DELEGATE_RESULT = RuleResult.noAction(new Object()); + private static final RuleResult ALTERNATIVE_RESULT = RuleResult.noAction(new Object()); + + @Mock(strictness = LENIENT) + private Rule delegate; + + @Mock(strictness = LENIENT) + private Rule alternative; + + private AlternativeActionRule target; + + @BeforeEach + public void setUp() { + target = AlternativeActionRule.of(delegate, alternative); + } + + @Test + public void processShouldReturnDelegateResult() { + // given + given(delegate.process(any(), any())).willReturn(DELEGATE_RESULT); + given(alternative.process(any(), any())).willReturn(ALTERNATIVE_RESULT); + + // when + final RuleResult result = target.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(DELEGATE_RESULT); + verifyNoInteractions(alternative); + } + + @Test + public void processShouldReturnAlternativeResultWhenNoMatchingRuleException() { + // given + given(delegate.process(any(), any())).willThrow(new NoMatchingRuleException()); + given(alternative.process(any(), any())).willReturn(ALTERNATIVE_RESULT); + + // when + final RuleResult result = target.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(ALTERNATIVE_RESULT); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java new file mode 100644 index 00000000000..a53fd44a31b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/CompositeRuleTest.java @@ -0,0 +1,63 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; + +import static java.util.Arrays.asList; +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.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class CompositeRuleTest { + + private static final Object VALUE = new Object(); + + @Test + public void processShouldAccumulateResultFromAllSubrules() { + // given + final Rule firstRule = (Rule) mock(Rule.class); + given(firstRule.process(any(), any())).willAnswer(invocationOnMock -> RuleResult.of( + invocationOnMock.getArgument(0), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("firstActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))))); + + final Rule secondRule = (Rule) mock(Rule.class); + given(secondRule.process(any(), any())).willAnswer(invocationOnMock -> RuleResult.of( + invocationOnMock.getArgument(0), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("secondActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))))); + + final CompositeRule target = CompositeRule.of(asList(firstRule, secondRule)); + + // when + final RuleResult result = target.process(VALUE, new Object()); + + // then + final Tags expectedTags = TagsImpl.of( + asList(ActivityImpl.of("firstActivity", "success", emptyList()), + ActivityImpl.of("secondActivity", "success", emptyList()))); + + final List expectedNonBids = List.of( + SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID))), + SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + + assertThat(result).isEqualTo(RuleResult.of(VALUE, RuleAction.UPDATE, expectedTags, expectedNonBids)); + + verify(firstRule).process(eq(VALUE), any()); + verify(secondRule).process(eq(VALUE), any()); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java new file mode 100644 index 00000000000..5b1bd6186d4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/ConditionalRuleTest.java @@ -0,0 +1,176 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +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.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.InfrastructureArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.Schema; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.schema.SchemaFunctionHolder; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.LookupResult; +import org.prebid.server.hooks.modules.rule.engine.core.rules.tree.RuleTree; +import org.prebid.server.hooks.v1.analytics.Tags; +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.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; + +@ExtendWith(MockitoExtension.class) +public class ConditionalRuleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final String ANALYTICS_KEY = "analyticsKey"; + private static final String MODEL_VERSION = "modelVersion"; + + private ConditionalRule target; + + @Mock(strictness = LENIENT) + private Schema schema; + + @Mock(strictness = LENIENT) + private RuleTree> ruleTree; + + @Mock(strictness = LENIENT) + private SchemaFunction firstSchemaFunction; + @Mock(strictness = LENIENT) + private SchemaFunction secondSchemaFunction; + @Mock(strictness = LENIENT) + private ResultFunction firstResultFunction; + @Mock(strictness = LENIENT) + private ResultFunction secondResultFunction; + + @BeforeEach + public void setUp() { + target = new ConditionalRule<>(schema, ruleTree, ANALYTICS_KEY, MODEL_VERSION); + } + + @Test + public void processShouldCorrectlyProcessData() { + // given + final Object value = new Object(); + final Object context = new Object(); + + // two schema functions + final ObjectNode firstSchemaFunctionConfig = MAPPER.createObjectNode(); + final ObjectNode secondSchemaFunctionConfig = MAPPER.createObjectNode(); + final String firstSchemaFunctionName = "firstFunction"; + final String secondSchemaFunctionName = "secondFunction"; + final String firstSchemaFunctionOutput = "firstSchemaOutput"; + final String secondSchemaFunctionOutput = "secondSchemaOutput"; + + given(schema.getFunctions()).willReturn(List.of( + SchemaFunctionHolder.of(firstSchemaFunctionName, firstSchemaFunction, firstSchemaFunctionConfig), + SchemaFunctionHolder.of(secondSchemaFunctionName, secondSchemaFunction, secondSchemaFunctionConfig))); + + given(firstSchemaFunction.extract(eq(SchemaFunctionArguments.of(value, firstSchemaFunctionConfig, context)))) + .willReturn(firstSchemaFunctionOutput); + given(secondSchemaFunction.extract(eq(SchemaFunctionArguments.of(value, secondSchemaFunctionConfig, context)))) + .willReturn(secondSchemaFunctionOutput); + + // two result functions + final String firstRuleActionName = "firstRuleAction"; + final String secondRuleActionName = "secondRuleAction"; + final ObjectNode firstResultFunctionConfig = MAPPER.createObjectNode(); + final ObjectNode secondResultFunctionConfig = MAPPER.createObjectNode(); + final List> resultFunctionHolders = List.of( + ResultFunctionHolder.of(firstRuleActionName, firstResultFunction, firstResultFunctionConfig), + ResultFunctionHolder.of(secondRuleActionName, secondResultFunction, secondResultFunctionConfig)); + + final RuleConfig ruleConfig = RuleConfig.of("ruleCondition", resultFunctionHolders); + + // tree that matches values based on schema functions outputs + final String firstDimensionMatch = "firstMatch"; + final String secondDimensionMatch = "secondMatch"; + given(ruleTree.lookup(eq(List.of(firstSchemaFunctionOutput, secondSchemaFunctionOutput)))) + .willReturn(LookupResult.of(ruleConfig, List.of(firstDimensionMatch, secondDimensionMatch))); + + // infrastructure arguments passed to result functions + final InfrastructureArguments infrastructureArguments = InfrastructureArguments.builder() + .context(context) + .schemaFunctionResults( + Map.of(firstSchemaFunctionName, firstSchemaFunctionOutput, + secondSchemaFunctionName, secondSchemaFunctionOutput)) + .schemaFunctionMatches( + Map.of(firstSchemaFunctionName, firstDimensionMatch, + secondSchemaFunctionName, secondDimensionMatch)) + .ruleFired(ruleConfig.getCondition()) + .analyticsKey(ANALYTICS_KEY) + .modelVersion(MODEL_VERSION) + .build(); + + // result of first result function processing + final Object firstResultFunctionUpdatedValue = new Object(); + final Tags firstTags = TagsImpl.of( + Collections.singletonList( + ActivityImpl.of("firstActivity", "status", Collections.emptyList()))); + final List firstSeatNonBids = Collections.singletonList( + SeatNonBid.of( + "seatA", + Collections.singletonList(NonBid.of("impIdA", BidRejectionReason.NO_BID)))); + + final RuleResult firstResultFunctionOutput = RuleResult.of( + firstResultFunctionUpdatedValue, + RuleAction.UPDATE, + firstTags, + firstSeatNonBids); + + final ResultFunctionArguments firstResultFunctionArgs = ResultFunctionArguments.of( + value, firstResultFunctionConfig, infrastructureArguments); + + given(firstResultFunction.apply(eq(firstResultFunctionArgs))).willReturn(firstResultFunctionOutput); + + // result of second result function processing + final Object secondResultFunctionUpdatedValue = new Object(); + final Tags secondTags = TagsImpl.of( + Collections.singletonList( + ActivityImpl.of("secondActivity", "status", Collections.emptyList()))); + final List secondSeatNonBids = Collections.singletonList( + SeatNonBid.of( + "seatB", + Collections.singletonList(NonBid.of("impIdB", BidRejectionReason.NO_BID)))); + + final RuleResult secondResultFunctionOutput = RuleResult.of( + secondResultFunctionUpdatedValue, + RuleAction.UPDATE, + secondTags, + secondSeatNonBids); + + final ResultFunctionArguments secondResultFunctionArgs = ResultFunctionArguments.of( + firstResultFunctionOutput.getValue(), + secondResultFunctionConfig, + infrastructureArguments); + + given(secondResultFunction.apply(eq(secondResultFunctionArgs))).willReturn(secondResultFunctionOutput); + + // when + final RuleResult result = target.process(value, context); + + // then + assertThat(result).isEqualTo( + RuleResult.of( + secondResultFunctionOutput.getValue(), + RuleAction.UPDATE, + TagsImpl.of(ListUtil.union(firstTags.activities(), secondTags.activities())), + ListUtil.union(firstSeatNonBids, secondSeatNonBids))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java new file mode 100644 index 00000000000..2a9fa1ceec4 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/DefaultActionRuleTest.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionArguments; +import org.prebid.server.hooks.modules.rule.engine.core.rules.result.ResultFunctionHolder; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; + +import java.util.List; + +import static java.util.Arrays.asList; +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; + +public class DefaultActionRuleTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + public void processShouldAccumulateResultFromAllRuleActions() { + // given + final Object value = new Object(); + final Object context = new Object(); + + final ObjectNode firstConfig = MAPPER.createObjectNode().set("config", TextNode.valueOf("test")); + final ResultFunction firstFunction = + (ResultFunction) mock(ResultFunction.class); + given(firstFunction.apply(any())).willAnswer(invocationOnMock -> RuleResult.of( + ((ResultFunctionArguments) invocationOnMock.getArgument(0)).getOperand(), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("firstActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID)))))); + + final ObjectNode secondConfig = MAPPER.createObjectNode().set("config", TextNode.valueOf("anotherTest")); + final ResultFunction secondFunction = + (ResultFunction) mock(ResultFunction.class); + given(secondFunction.apply(any())).willAnswer(invocationOnMock -> RuleResult.of( + ((ResultFunctionArguments) invocationOnMock.getArgument(0)).getOperand(), + RuleAction.UPDATE, + TagsImpl.of(singletonList(ActivityImpl.of("secondActivity", "success", emptyList()))), + singletonList(SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))))); + + final List> actions = List.of( + ResultFunctionHolder.of("firstFunction", firstFunction, firstConfig), + ResultFunctionHolder.of("secondFunction", secondFunction, secondConfig)); + + final DefaultActionRule target = new DefaultActionRule<>( + actions, "analyticsKey", "modelVersion"); + + // when + final RuleResult result = target.process(value, context); + + // then + final Tags expectedTags = TagsImpl.of( + asList(ActivityImpl.of("firstActivity", "success", emptyList()), + ActivityImpl.of("secondActivity", "success", emptyList()))); + + final List expectedNonBids = List.of( + SeatNonBid.of("firstSeat", singletonList(NonBid.of("1", BidRejectionReason.NO_BID))), + SeatNonBid.of("secondSeat", singletonList(NonBid.of("2", BidRejectionReason.NO_BID)))); + + assertThat(result).isEqualTo(RuleResult.of(value, RuleAction.UPDATE, expectedTags, expectedNonBids)); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java new file mode 100644 index 00000000000..b9304f40b55 --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/WeightedRuleTest.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.rule.engine.core.util.WeightedList; + +import java.util.random.RandomGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class WeightedRuleTest { + + @Test + public void processShouldUtilizeRuleFromWeightedList() { + // given + final WeightedList> ruleList = + (WeightedList>) mock(WeightedList.class); + final RuleResult stub = RuleResult.noAction(new Object()); + given(ruleList.getForSeed(anyInt())).willReturn((left, right) -> stub); + + final RandomGenerator randomGenerator = mock(RandomGenerator.class); + given(randomGenerator.nextDouble()).willReturn(0.5); + + final RandomWeightedRule rule = RandomWeightedRule.of(randomGenerator, ruleList); + + // when + final RuleResult result = rule.process(new Object(), new Object()); + + // then + assertThat(result).isEqualTo(stub); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java new file mode 100644 index 00000000000..9275612dd2b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/core/rules/tree/RuleTreeTest.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.rule.engine.core.rules.tree; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.rule.engine.core.rules.exception.NoMatchingRuleException; + +import java.util.List; +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.Assertions.assertThatExceptionOfType; + +public class RuleTreeTest { + + @Test + public void getValueShouldReturnExpectedValue() { + // given + final Map> subnodes = Map.of( + "A", + new RuleNode.IntermediateNode<>( + Map.of("B", new RuleNode.LeafNode<>("AB"), "*", new RuleNode.LeafNode<>("AC"))), + "B", + new RuleNode.IntermediateNode<>( + Map.of("B", new RuleNode.LeafNode<>("BB"), "C", new RuleNode.LeafNode<>("BC")))); + + final RuleTree tree = new RuleTree<>(new RuleNode.IntermediateNode<>(subnodes), 2); + + // when and then + assertThat(tree.lookup(asList("A", "B"))).isEqualTo(LookupResult.of("AB", List.of("A", "B"))); + assertThat(tree.lookup(asList("A", "C"))).isEqualTo(LookupResult.of("AC", List.of("A", "*"))); + assertThat(tree.lookup(asList("B", "B"))).isEqualTo(LookupResult.of("BB", List.of("B", "B"))); + assertThat(tree.lookup(asList("B", "C"))).isEqualTo(LookupResult.of("BC", List.of("B", "C"))); + assertThatExceptionOfType(NoMatchingRuleException.class).isThrownBy(() -> tree.lookup(asList("C", "B"))); + assertThatExceptionOfType(NoMatchingRuleException.class).isThrownBy(() -> tree.lookup(singletonList("C"))); + } +} diff --git a/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..97e7d19e18b --- /dev/null +++ b/extra/modules/pb-rule-engine/src/test/java/org/prebid/server/hooks/modules/rule/engine/v1/PbRuleEngineProcessedAuctionRequestHookTest.java @@ -0,0 +1,170 @@ +package org.prebid.server.hooks.modules.rule.engine.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.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.ImpRejection; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.modules.rule.engine.core.config.RuleParser; +import org.prebid.server.hooks.modules.rule.engine.core.request.Granularity; +import org.prebid.server.hooks.modules.rule.engine.core.request.RequestRuleContext; +import org.prebid.server.hooks.modules.rule.engine.core.rules.PerStageRule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.Rule; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleAction; +import org.prebid.server.hooks.modules.rule.engine.core.rules.RuleResult; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.NonBid; +import org.prebid.server.proto.openrtb.ext.response.seatnonbid.SeatNonBid; +import org.prebid.server.settings.model.Account; + +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +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; + +@ExtendWith(MockitoExtension.class) +class PbRuleEngineProcessedAuctionRequestHookTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private PbRuleEngineProcessedAuctionRequestHook target; + + @Mock(strictness = LENIENT) + private RuleParser ruleParser; + + @Mock(strictness = LENIENT) + private AuctionRequestPayload payload; + + @Mock(strictness = LENIENT) + private AuctionInvocationContext invocationContext; + + @Mock(strictness = LENIENT) + private Rule processedAuctionRequestRule; + + @Mock(strictness = LENIENT) + private BidRequest bidRequest; + + @Mock(strictness = LENIENT) + private Tags tags; + + private final AuctionContext auctionContext = AuctionContext.builder().account(Account.empty("1001")).build(); + + @BeforeEach + void setUp() { + target = new PbRuleEngineProcessedAuctionRequestHook(ruleParser, "datacenter"); + + given(invocationContext.auctionContext()).willReturn(auctionContext); + given(payload.bidRequest()).willReturn(bidRequest); + + given(ruleParser.parseForAccount(any(), any())).willReturn( + Future.succeededFuture( + PerStageRule.builder() + .timestamp(Instant.EPOCH) + .processedAuctionRequestRule(processedAuctionRequestRule) + .build())); + } + + @Test + public void callShouldReturnNoActionWhenNoAccountConfigProvided() { + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } + + @Test + public void callShouldReturnNoActionWhenRuleActionIsNoAction() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.noAction(bidRequest)); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } + + @Test + public void callShouldReturnPayloadUpdateWhenRuleActionIsUpdate() { + // given + final SeatNonBid seatNonBid = SeatNonBid.of( + "bidder", Collections.singletonList(NonBid.of("impId", BidRejectionReason.NO_BID))); + + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.of(bidRequest, RuleAction.UPDATE, tags, Collections.singletonList(seatNonBid))); + + // when and then + final Map> rejections = Map.of( + "bidder", List.of(ImpRejection.of("impId", BidRejectionReason.NO_BID))); + + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.update); + assertThat(invocationResult.rejections()).containsExactlyEntriesOf(rejections); + assertThat(invocationResult.payloadUpdate()).isNotNull(); + assertThat(invocationResult.analyticsTags()).isEqualTo(tags); + }); + } + + @Test + public void callShouldReturnRejectWhenRuleActionIsReject() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willReturn(RuleResult.rejected(tags, Collections.emptyList())); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.reject); + assertThat(invocationResult.payloadUpdate()).isNull(); + assertThat(invocationResult.analyticsTags()).isEqualTo(tags); + }); + } + + @Test + public void callShouldReturnFailureOnFailure() { + // given + given(invocationContext.accountConfig()).willReturn(MAPPER.createObjectNode()); + given(processedAuctionRequestRule.process( + bidRequest, + RequestRuleContext.of(auctionContext, Granularity.Request.instance(), "datacenter"))) + .willThrow(PreBidException.class); + + // when and then + assertThat(target.call(payload, invocationContext).result()).satisfies(invocationResult -> { + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.payloadUpdate()).isNull(); + }); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 32f53d9ae05..e18fe0f665e 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.39.0-SNAPSHOT ../../extra/pom.xml @@ -20,57 +20,22 @@ ortb2-blocking confiant-ad-quality pb-richmedia-filter + fiftyone-devicedetection + pb-response-correction + greenbids-real-time-data + pb-request-correction + optable-targeting + wurfl-devicedetection + live-intent-omni-channel-identity + pb-rule-engine - - 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 +46,8 @@ prebid-server - org.junit.vintage - junit-vintage-engine + org.junit.jupiter + junit-jupiter-engine test @@ -91,13 +56,13 @@ test - junit - junit + org.mockito + mockito-core test org.mockito - mockito-core + mockito-junit-jupiter test @@ -114,6 +79,9 @@ org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} + + ${skipUnitTests} + diff --git a/extra/modules/wurfl-devicedetection/README.md b/extra/modules/wurfl-devicedetection/README.md new file mode 100644 index 00000000000..2a5f9959c03 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/README.md @@ -0,0 +1,250 @@ +## WURFL Device Enrichment Module + +### Overview + +The **WURFL Device Enrichment Module** for Prebid Server enhances the OpenRTB 2.x payload +with comprehensive device detection data powered by **ScientiaMobile**’s WURFL device detection framework. +Thanks to WURFL's device knowledge, the module provides accurate and comprehensive device-related information, +enabling bidders to make better-informed targeting and optimization decisions. + +### Key features + +#### Device Field Enrichment: + +The WURFL module populates missing or empty fields in ortb2.device with the following data: + - **make**: Manufacturer of the device (e.g., "Apple", "Samsung"). + - **model**: Device model (e.g., "iPhone 14", "Galaxy S22"). + - **os**: Operating system (e.g., "iOS", "Android"). + - **osv**: Operating system version (e.g., "16.0", "12.0"). + - **h**: Screen height in pixels. + - **w**: Screen width in pixels. + - **ppi**: Screen pixels per inch (PPI). + - **pxratio**: Screen pixel density ratio. + - **devicetype**: Device type (e.g., mobile, tablet, desktop). + - **js**: Support for JavaScript, where 0 = no, 1 = yes + - **Note**: If these fields are already populated in the bid request, the module will not overwrite them. + +#### Publisher-Specific Enrichment: + +Device enrichment is selectively enabled for publishers based on their account ID. +The module identifies publishers through the following fields: + +`site.publisher.id` (for web environments). +`app.publisher.id` (for mobile app environments). +`dooh.publisher.id` (for digital out-of-home environments). + + +### Building WURFL Module with a licensed WURFL Onsite Java API + +In order to compile the WURFL module in the PBS Java server bundle using a licensed WURFL API, you must follow these steps: + +1 - Change the URL in the `` tag in the module's `pom.xml` file to the ScientiaMobile Maven repository URL: + +`https://maven.scientiamobile.com/repository/wurfl-onsite/` + +The repository is private and requires authentication: to set it up please check the paragraph +"Configuring your Builds to work with ScientiaMobile's Private Maven Repository" +[on this page](https://docs.scientiamobile.com/documentation/onsite/onsite-java-api). + +2 - Change the `artfactId` value in the module's `pom.xml` from `wurfl-mock` to `wurfl` + +3 - Update the `wurfl.version` property value to the latest WURFL Onsite Java API version available. + + +When the `pom.xml` references the mock API artifact, the module will compile a demo version that returns sample data, +allowing basic testing without an WURFL Onsite Java API license. + +4 - Build the Prebid Server Java bundle with the WURFL module using the following command: + +```bash +mvn clean package --file extra/pom.xml +``` + +### Configuring the WURFL Module + +Below is a sample configuration for the WURFL module: + +```yaml +hooks: + wurfl-devicedetection: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-raw-auction-request" + } + ] + } + ] + } + } + } + } + } + modules: + wurfl-devicedetection: + file-dir-path: + file-snapshot-url: https://data.scientiamobile.com//wurfl.zip + cache-size: 200000 + update-frequency-in-hours: 24 + allowed-publisher-ids: 1 + ext-caps: false +``` + +### Configuration Options + +| Parameter | Requirement | Description | +|---------------------------------|-------------|---------------------------------------------------------------------------------------------------| +| **`file-dir-path`** | Mandatory | Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. | +| **`file-snapshot-url`** | Mandatory | URL of the licensed WURFL snapshot file to be downloaded when Prebid Server Java starts. | +| **`cache-size`** | Optional | Maximum number of devices stored in the WURFL cache. Defaults to the WURFL cache's standard size. | +| **`ext-caps`** | Optional | If `true`, the module adds all licensed capabilities to the `device.ext` object. | +| **`update-frequency-in-hours`** | Optional | Check interval (hours) for downloading updated wurfl file if modified. Defaults to 24 hours | +| **`allowed-publisher-ids`** | Optional | List of publisher IDs permitted to use the module. Defaults to all publishers. | + + +A valid WURFL license must include all the required capabilities for device enrichment. + +### Launching Prebid Server Java with the WURFL Module + +After configuring the module and successfully building the Prebid Server bundle, start the server with the following command: + +```bash +java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/configs/prebid-config-with-wurfl.yaml +``` + +This sample configuration contains the module hook basic configuration. + +When the server starts, it downloads the WURFL file from the `wurfl-snapshot-url` and loads it into the module. + +Sample request data for testing is available in the module's `sample` directory. Using the `auction` endpoint, +you can observe WURFL-enriched device data in the response. + +### Sample Response + +Using the sample request data via `curl` when the module is configured with `ext-caps` set to `false` (or no value) + +```bash +curl http://localhost:8080/openrtb2/auction --data @extra/modules/wurfl-devicedetection/sample/request_data.json +``` + +the device object in the response will include WURFL device detection data: + +```json +"device": { + "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015;) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64", + "devicetype": 1, + "make": "Google", + "model": "Pixel 9 Pro XL", + "os": "Android", + "osv": "15", + "h": 2992, + "w": 1344, + "ppi": 481, + "pxratio": 2.55, + "js": 1, + "ext": { + "wurfl": { + "wurfl_id": "google_pixel_9_pro_xl_ver1_suban150" + } + } +} +``` + +When `ext_caps` is set to `true`, the response will include all licensed capabilities: + +```json +"device":{ + "ua":"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64", + "devicetype":1, + "make":"Google", + "model":"Pixel 9 Pro XL", + "os":"Android", + "osv":"15", + "h":2992, + "w":1344, + "ppi":481, + "pxratio":2.55, + "js":1, + "ext":{ + "wurfl":{ + "wurfl_id":"google_pixel_9_pro_xl_ver1_suban150", + "mobile_browser_version":"", + "resolution_height":"2992", + "resolution_width":"1344", + "is_wireless_device":"true", + "is_tablet":"false", + "physical_form_factor":"phone_phablet", + "ajax_support_javascript":"true", + "preferred_markup":"html_web_4_0", + "brand_name":"Google", + "can_assign_phone_number":"true", + "xhtml_support_level":"4", + "ux_full_desktop":"false", + "device_os":"Android", + "physical_screen_width":"71", + "is_connected_tv":"false", + "is_smarttv":"false", + "physical_screen_height":"158", + "model_name":"Pixel 9 Pro XL", + "is_ott":"false", + "density_class":"2.55", + "marketing_name":"", + "device_os_version":"15.0", + "mobile_browser":"Chrome Mobile", + "pointing_method":"touchscreen", + "is_app_webview":"false", + "advertised_app_name":"Edge Browser", + "is_smartphone":"true", + "is_robot":"false", + "advertised_device_os":"Android", + "is_largescreen":"true", + "is_android":"true", + "is_xhtmlmp_preferred":"false", + "device_name":"Google Pixel 9 Pro XL", + "is_ios":"false", + "is_touchscreen":"true", + "is_wml_preferred":"false", + "is_app":"false", + "is_mobile":"true", + "is_phone":"true", + "is_full_desktop":"false", + "is_generic":"false", + "advertised_browser":"Edge", + "complete_device_name":"Google Pixel 9 Pro XL", + "advertised_browser_version":"124.0.2478.64", + "is_html_preferred":"true", + "is_windows_phone":"false", + "pixel_density":"481", + "form_factor":"Smartphone", + "advertised_device_os_version":"15" + } + } +} +``` + +## Maintainer + +prebid@scientiamobile.com diff --git a/extra/modules/wurfl-devicedetection/pom.xml b/extra/modules/wurfl-devicedetection/pom.xml new file mode 100644 index 00000000000..1068dc04cd4 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.39.0-SNAPSHOT + + + wurfl-devicedetection + + wurfl-devicedetection + WURFL device detection and data enrichment module + + + 1.0.0.0 + + + + + com.scientiamobile.wurfl + https://maven.scientiamobile.com/repository/wurfl-onsite-tools/ + + + + + + com.scientiamobile.wurfl + wurfl-mock + ${wurfl.version} + + + diff --git a/extra/modules/wurfl-devicedetection/sample/request_data.json b/extra/modules/wurfl-devicedetection/sample/request_data.json new file mode 100644 index 00000000000..42691bbc74d --- /dev/null +++ b/extra/modules/wurfl-devicedetection/sample/request_data.json @@ -0,0 +1,119 @@ +{ + "imp": [ + { + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "test" + }, + "pbadslot": "test", + "gpid": "test" + }, + "gpid": "test", + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 1, + "use_pmt_rule": false + }, + "0test": { + "placement_id": 1 + } + }, + "adunitcode": "25e8ad9f-13a4-4404-ba74-f9eebff0e86c", + "floors": { + "floorMin": 0.01 + } + } + }, + "id": "2529eeea-813e-4da6-838f-f91c28d64867", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + } + ], + "site": { + "domain": "test.com", + "publisher": { + "domain": "test.com", + "id": "1" + }, + "page": "https://www.test.com/" + }, + "device": { + "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64" + }, + "id": "fc4670ce-4985-4316-a245-b43c885dc37a", + "test": 1, + "cur": [ + "USD" + ], + "source": { + "ext": { + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + }, + "ext": { + "prebid": { + "cache": { + "bids": { + "returnCreative": true + }, + "vastxml": { + "returnCreative": true + } + }, + "auctiontimestamp": 1799310801804, + "targeting": { + "includewinners": true, + "includebidderkeys": false + }, + "schains": [ + { + "bidders": [ + "appnexus" + ], + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + ], + "floors": { + "enabled": false, + "floorMin": 0.01, + "floorMinCur": "USD" + }, + "createtids": false + } + }, + "user": {}, + "tmax": 2000 +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java new file mode 100644 index 00000000000..116095f5a08 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java @@ -0,0 +1,32 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config; + +import lombok.Data; + +import java.util.Collections; +import java.util.Set; + +@Data +public class WURFLDeviceDetectionConfigProperties { + + private static final int DEFAULT_UPDATE_TIMEOUT = 5000; + private static final long DEFAULT_RETRY_INTERVAL = 200L; + private static final int DEFAULT_UPDATE_RETRIES = 3; + + int cacheSize; + + String fileDirPath; + + String fileSnapshotUrl; + + boolean extCaps; + + int updateFrequencyInHours; + + Set allowedPublisherIds = Collections.emptySet(); + + int updateConnTimeoutMs = DEFAULT_UPDATE_TIMEOUT; + + int updateRetries = DEFAULT_UPDATE_RETRIES; + + long retryIntervalMs = DEFAULT_RETRY_INTERVAL; +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java new file mode 100644 index 00000000000..809dad7b19a --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java @@ -0,0 +1,100 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config; + +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineUtils; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionEntrypointHook; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionModule; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionRawAuctionRequestHook; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.syncer.FileSyncer; +import org.prebid.server.spring.config.model.FileSyncerProperties; +import org.prebid.server.spring.config.model.HttpClientProperties; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.json.JacksonMapper; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.nio.file.Path; +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + WURFLDeviceDetectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +public class WURFLDeviceDetectionConfiguration { + + private static final Long HOUR_IN_MILLIS = 3600000L; + private static final int DEFAULT_UPDATE_FREQ_IN_HOURS = 24; + + @Bean + @ConfigurationProperties(prefix = "hooks.modules." + WURFLDeviceDetectionModule.CODE) + WURFLDeviceDetectionConfigProperties configProperties() { + return new WURFLDeviceDetectionConfigProperties(); + } + + @Bean + public WURFLDeviceDetectionModule wurflDeviceDetectionModule(WURFLDeviceDetectionConfigProperties configProperties, + JacksonMapper mapper, + Vertx vertx) { + + final WURFLService wurflService = new WURFLService(null, configProperties); + final FileSyncer fileSyncer = createFileSyncer(configProperties, wurflService, vertx); + fileSyncer.sync(); + + return new WURFLDeviceDetectionModule(List.of( + new WURFLDeviceDetectionEntrypointHook(), + new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper))); + } + + private FileSyncer createFileSyncer(WURFLDeviceDetectionConfigProperties configProperties, + WURFLService wurflService, + Vertx vertx) { + + final FileSyncerProperties fileSyncerProperties = createFileSyncerProperties(configProperties); + return FileUtil.fileSyncerFor(wurflService, fileSyncerProperties, vertx); + } + + private FileSyncerProperties createFileSyncerProperties(WURFLDeviceDetectionConfigProperties configProperties) { + final String downloadPath = createDownloadPath(configProperties); + final String tempPath = createTempPath(configProperties); + final HttpClientProperties httpProperties = createHttpProperties(configProperties); + + final FileSyncerProperties fileSyncerProperties = new FileSyncerProperties(); + fileSyncerProperties.setCheckSize(true); + fileSyncerProperties.setDownloadUrl(configProperties.getFileSnapshotUrl()); + fileSyncerProperties.setSaveFilepath(downloadPath); + fileSyncerProperties.setTmpFilepath(tempPath); + fileSyncerProperties.setTimeoutMs((long) configProperties.getUpdateConnTimeoutMs()); + fileSyncerProperties.setRetryCount(configProperties.getUpdateRetries()); + fileSyncerProperties.setRetryIntervalMs(configProperties.getRetryIntervalMs()); + fileSyncerProperties.setHttpClient(httpProperties); + int updateFreqInHours = configProperties.getUpdateFrequencyInHours(); + if (updateFreqInHours <= 0) { + updateFreqInHours = DEFAULT_UPDATE_FREQ_IN_HOURS; + } + final long syncIntervalMillis = updateFreqInHours * HOUR_IN_MILLIS; + fileSyncerProperties.setUpdateIntervalMs(syncIntervalMillis); + + return fileSyncerProperties; + } + + private String createTempPath(WURFLDeviceDetectionConfigProperties configProperties) { + final String basePath = configProperties.getFileDirPath(); + final String fileName = "tmp_" + + WURFLEngineUtils.extractWURFLFileName(configProperties.getFileSnapshotUrl()); + return Path.of(basePath, fileName).toString(); + } + + private String createDownloadPath(WURFLDeviceDetectionConfigProperties configProperties) { + final String basePath = configProperties.getFileDirPath(); + final String fileName = WURFLEngineUtils.extractWURFLFileName(configProperties.getFileSnapshotUrl()); + return Path.of(basePath, fileName).toString(); + } + + private HttpClientProperties createHttpProperties(WURFLDeviceDetectionConfigProperties configProperties) { + final HttpClientProperties httpProperties = new HttpClientProperties(); + httpProperties.setConnectTimeoutMs(configProperties.getUpdateConnTimeoutMs()); + httpProperties.setMaxRedirects(1); + return httpProperties; + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java new file mode 100644 index 00000000000..97f46c69ee6 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLDeviceDetectionException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc; + +public class WURFLDeviceDetectionException extends RuntimeException { + + public WURFLDeviceDetectionException(String message) { + super(message); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java new file mode 100644 index 00000000000..ee72b492bda --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import lombok.Value; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Value +public class AuctionRequestHeadersContext { + + Map headers; + + public static AuctionRequestHeadersContext from(CaseInsensitiveMultiMap headers) { + if (headers == null) { + return new AuctionRequestHeadersContext(Collections.emptyMap()); + } + + final Map headersMap = new HashMap<>(); + for (String headerName : headers.names()) { + headersMap.put(headerName, headers.getAll(headerName).getFirst()); + } + return new AuctionRequestHeadersContext(Collections.unmodifiableMap(headersMap)); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java new file mode 100644 index 00000000000..a1593e89131 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtils.java @@ -0,0 +1,85 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import com.scientiamobile.wurfl.core.GeneralWURFLEngine; +import com.scientiamobile.wurfl.core.WURFLEngine; +import com.scientiamobile.wurfl.core.cache.LRUMapCacheProvider; +import com.scientiamobile.wurfl.core.cache.NullCacheProvider; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLDeviceDetectionException; + +import java.net.URI; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class WURFLEngineUtils { + + private WURFLEngineUtils() { + } + + private static final Set REQUIRED_STATIC_CAPS = Set.of( + "ajax_support_javascript", + "brand_name", + "density_class", + "is_connected_tv", + "is_ott", + "is_tablet", + "model_name", + "resolution_height", + "resolution_width", + "physical_form_factor"); + + public static WURFLEngine initializeEngine(WURFLDeviceDetectionConfigProperties configProperties, + String wurflInFilePath) { + + final String wurflFilePath = wurflInFilePath != null + ? wurflInFilePath + : wurflFilePathFromConfig(configProperties); + + final WURFLEngine engine = new GeneralWURFLEngine(wurflFilePath); + verifyStaticCapabilitiesDefinition(engine); + + final int cacheSize = configProperties.getCacheSize(); + engine.setCacheProvider(cacheSize > 0 + ? new LRUMapCacheProvider(configProperties.getCacheSize()) + : new NullCacheProvider()); + + return engine; + } + + private static String wurflFilePathFromConfig(WURFLDeviceDetectionConfigProperties configProperties) { + final String wurflFileName = extractWURFLFileName(configProperties.getFileSnapshotUrl()); + return Paths.get(configProperties.getFileDirPath(), wurflFileName).toAbsolutePath().toString(); + } + + public static String extractWURFLFileName(String wurflSnapshotUrl) { + try { + final URI uri = new URI(wurflSnapshotUrl); + final String path = uri.getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid WURFL snapshot URL: " + wurflSnapshotUrl, e); + } + } + + private static void verifyStaticCapabilitiesDefinition(WURFLEngine engine) { + final List unsupportedStaticCaps = new ArrayList<>(); + for (String requiredCapName : REQUIRED_STATIC_CAPS) { + if (!engine.getAllCapabilities().contains(requiredCapName)) { + unsupportedStaticCaps.add(requiredCapName); + } + } + + if (!unsupportedStaticCaps.isEmpty()) { + Collections.sort(unsupportedStaticCaps); + final String failedCheckMessage = """ + Static capabilities %s needed for device enrichment are not defined in WURFL. + Please make sure that your license has the needed capabilities or upgrade it. + """.formatted(String.join(",", unsupportedStaticCaps)); + + throw new WURFLDeviceDetectionException(failedCheckMessage); + } + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java new file mode 100644 index 00000000000..a156b3a9b33 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java @@ -0,0 +1,104 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.UserAgent; +import org.prebid.server.util.HttpUtil; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class HeadersResolver { + + private HeadersResolver() { + } + + public static Map resolve(Device device, Map headers) { + if (device == null && headers == null) { + return Collections.emptyMap(); + } + + final Map resolvedHeaders = resolveFromDevice(device); + return MapUtils.isNotEmpty(resolvedHeaders) + ? resolvedHeaders + : headers; + } + + private static Map resolveFromDevice(Device device) { + if (device == null) { + return Collections.emptyMap(); + } + + final Map resolvedHeaders = new HashMap<>(); + if (device.getUa() != null) { + resolvedHeaders.put(HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); + } + resolvedHeaders.putAll(resolveFromSua(device.getSua())); + + return resolvedHeaders; + } + + private static Map resolveFromSua(UserAgent sua) { + if (sua == null) { + return Collections.emptyMap(); + } + + final List brands = sua.getBrowsers(); + if (CollectionUtils.isEmpty(brands)) { + return Collections.emptyMap(); + } + + final Map headers = new HashMap<>(); + final String brandList = brandListAsString(brands); + headers.put(HttpUtil.SEC_CH_UA.toString(), brandList); + headers.put(HttpUtil.SEC_CH_UA_FULL_VERSION_LIST.toString(), brandList); + + final BrandVersion platform = sua.getPlatform(); + if (platform != null) { + headers.put(HttpUtil.SEC_CH_UA_PLATFORM.toString(), platform.getBrand()); + headers.put(HttpUtil.SEC_CH_UA_PLATFORM_VERSION.toString(), versionFromTokens(platform.getVersion())); + } + + final String model = sua.getModel(); + if (StringUtils.isNotEmpty(model)) { + headers.put(HttpUtil.SEC_CH_UA_MODEL.toString(), model); + } + + final String arch = sua.getArchitecture(); + if (StringUtils.isNotEmpty(arch)) { + headers.put(HttpUtil.SEC_CH_UA_ARCH.toString(), arch); + } + + final Integer mobile = sua.getMobile(); + if (mobile != null) { + headers.put(HttpUtil.SEC_CH_UA_MOBILE.toString(), "?" + mobile); + } + + return headers; + } + + private static String brandListAsString(List versions) { + return versions.stream() + .filter(brandVersion -> brandVersion.getBrand() != null) + .map(brandVersion -> "\"%s\";v=\"%s\"".formatted( + brandVersion.getBrand(), + versionFromTokens(brandVersion.getVersion()))) + .collect(Collectors.joining(", ")); + } + + private static String versionFromTokens(List tokens) { + if (CollectionUtils.isEmpty(tokens)) { + return StringUtils.EMPTY; + } + + return tokens.stream() + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(".")); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java new file mode 100644 index 00000000000..ec79b79049c --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java @@ -0,0 +1,288 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.PayloadUpdate; + +import java.math.BigDecimal; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +public class OrtbDeviceUpdater implements PayloadUpdate { + + private static final Logger logger = LoggerFactory.getLogger(OrtbDeviceUpdater.class); + + private static final String WURFL_PROPERTY = "wurfl"; + + private final com.scientiamobile.wurfl.core.Device wurflDevice; + private final Set staticCaps; + private final Set virtualCaps; + private final boolean addExtCaps; + private final JacksonMapper mapper; + + public OrtbDeviceUpdater(com.scientiamobile.wurfl.core.Device wurflDevice, + Set staticCaps, + Set virtualCaps, + boolean addExtCaps, + JacksonMapper mapper) { + + this.wurflDevice = Objects.requireNonNull(wurflDevice); + this.staticCaps = Objects.requireNonNull(staticCaps); + this.virtualCaps = Objects.requireNonNull(virtualCaps); + this.addExtCaps = addExtCaps; + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload auctionRequestPayload) { + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + return AuctionRequestPayloadImpl.of(bidRequest.toBuilder() + .device(update(bidRequest.getDevice())) + .build()); + } + + private Device update(Device ortbDevice) { + final String make = tryUpdateField(ortbDevice.getMake(), this::getWurflMake); + final String model = tryUpdateField(ortbDevice.getModel(), this::getWurflModel); + final String hwv = tryUpdateField(ortbDevice.getHwv(), this::getWurflModel); + final Integer deviceType = tryUpdateField( + Optional.ofNullable(ortbDevice.getDevicetype()) + .filter(it -> it > 0) + .orElse(null), + this::getWurflDeviceType); + final String os = tryUpdateField(ortbDevice.getOs(), this::getWurflOs); + final String osv = tryUpdateField(ortbDevice.getOsv(), this::getWurflOsv); + final Integer h = tryUpdateField(ortbDevice.getH(), this::getWurflH); + final Integer w = tryUpdateField(ortbDevice.getW(), this::getWurflW); + final Integer ppi = tryUpdateField(ortbDevice.getPpi(), this::getWurflPpi); + final BigDecimal pxratio = tryUpdateField(ortbDevice.getPxratio(), this::getWurflPxRatio); + final Integer js = tryUpdateField(ortbDevice.getJs(), this::getWurflJs); + + return ortbDevice.toBuilder() + .make(make) + .model(model) + .devicetype(deviceType) + .hwv(hwv) + .os(os) + .osv(osv) + .h(h) + .w(w) + .ppi(ppi) + .pxratio(pxratio) + .js(js) + .ext(updateExt(ortbDevice.getExt())) + .build(); + } + + private static T tryUpdateField(T fromOrtbDevice, Supplier fromWurflDeviceSupplier) { + if (fromOrtbDevice != null) { + return fromOrtbDevice; + } + + final T fromWurflDevice = fromWurflDeviceSupplier.get(); + return fromWurflDevice != null + ? fromWurflDevice + : fromOrtbDevice; + } + + private String getWurflMake() { + return wurflDevice.getCapability("brand_name"); + } + + private String getWurflModel() { + return wurflDevice.getCapability("model_name"); + } + + private Integer getWurflDeviceType() { + + if (getWurflIsOtt()) { + return 7; + } + + if (getWurflIsConsole()) { + return 6; + } + + if ("out_of_home_device".equals(getWurflPhysicalFormFactor())) { + return 8; + } + + final String formFactor = getWurflFormFactor(); + return switch (formFactor) { + case "Desktop" -> 2; + case "Smartphone", "Feature Phone" -> 4; + case "Tablet" -> 5; + case "Smart-TV" -> 3; + case "Other Non-Mobile" -> 6; + case "Other Mobile" -> 1; + default -> null; + }; + } + + private Boolean getWurflIsOtt() { + try { + return wurflDevice.getCapabilityAsBool("is_ott"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get is_ott from WURFL device capabilities"); + return Boolean.FALSE; + } + } + + private String getWurflFormFactor() { + try { + return wurflDevice.getVirtualCapability("form_factor"); + } catch (VirtualCapabilityNotDefinedException e) { + logger.warn("Failed to get form_factor from WURFL device capabilities"); + return ""; + } + } + + private String getWurflPhysicalFormFactor() { + try { + return wurflDevice.getCapability("physical_form_factor"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get physical_form_factor from WURFL device capabilities"); + return ""; + } + } + + private Boolean getWurflIsConsole() { + try { + return wurflDevice.getCapabilityAsBool("is_console"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get is_console from WURFL device capabilities"); + return Boolean.FALSE; + } + } + + private String getWurflOs() { + try { + return wurflDevice.getVirtualCapability("advertised_device_os"); + } catch (VirtualCapabilityNotDefinedException e) { + logger.warn("Failed to evaluate advertised device OS"); + return null; + } + } + + private String getWurflOsv() { + try { + return wurflDevice.getVirtualCapability("advertised_device_os_version"); + } catch (VirtualCapabilityNotDefinedException e) { + logger.warn("Failed to evaluate advertised device OS version"); + } + return null; + } + + private Integer getWurflH() { + try { + return wurflDevice.getCapabilityAsInt("resolution_height"); + } catch (NumberFormatException e) { + logger.warn("Failed to get resolution height from WURFL device capabilities"); + return null; + } + } + + private Integer getWurflW() { + try { + return wurflDevice.getCapabilityAsInt("resolution_width"); + } catch (NumberFormatException e) { + logger.warn("Failed to get resolution width from WURFL device capabilities"); + return null; + } + } + + private Integer getWurflPpi() { + try { + return wurflDevice.getVirtualCapabilityAsInt("pixel_density"); + } catch (VirtualCapabilityNotDefinedException e) { + logger.warn("Failed to get pixel density from WURFL device capabilities"); + return null; + } + } + + private BigDecimal getWurflPxRatio() { + try { + final String densityAsString = wurflDevice.getCapability("density_class"); + return densityAsString != null + ? new BigDecimal(densityAsString) + : null; + } catch (CapabilityNotDefinedException | NumberFormatException e) { + logger.warn("Failed to get pixel ratio from WURFL device capabilities"); + return null; + } + } + + private Integer getWurflJs() { + try { + return wurflDevice.getCapabilityAsBool("ajax_support_javascript") ? 1 : 0; + } catch (CapabilityNotDefinedException | NumberFormatException e) { + logger.warn("Failed to get JS support from WURFL device capabilities"); + return null; + } + } + + private ExtDevice updateExt(ExtDevice ortbExtDevice) { + if (ortbExtDevice != null && ortbExtDevice.containsProperty(WURFL_PROPERTY)) { + return ortbExtDevice; + } + + final ExtDevice updatedExt = Optional.ofNullable(ortbExtDevice) + .map(this::copyExtDevice) + .orElse(ExtDevice.empty()); + + updatedExt.addProperty(WURFL_PROPERTY, createWurflObject()); + + return updatedExt; + } + + private ExtDevice copyExtDevice(ExtDevice original) { + final ExtDevice copy = ExtDevice.of(original.getAtts(), original.getPrebid()); + mapper.fillExtension(copy, original); + return copy; + } + + private ObjectNode createWurflObject() { + final ObjectNode wurfl = mapper.mapper().createObjectNode(); + + wurfl.put("wurfl_id", wurflDevice.getId()); + + if (!addExtCaps) { + return wurfl; + } + + for (String capability : staticCaps) { + try { + final String value = wurflDevice.getCapability(capability); + if (value != null) { + wurfl.put(capability, value); + } + } catch (Exception e) { + logger.warn("Error getting capability for {}: {}", capability, e.getMessage()); + } + } + + for (String virtualCapability : virtualCaps) { + try { + final String value = wurflDevice.getVirtualCapability(virtualCapability); + if (value != null) { + wurfl.put(virtualCapability, value); + } + } catch (Exception e) { + logger.warn("Could not fetch virtual capability {}", virtualCapability); + } + } + + return wurfl; + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java new file mode 100644 index 00000000000..102d4a164b2 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import io.vertx.core.Future; + +public class WURFLDeviceDetectionEntrypointHook implements EntrypointHook { + + private static final String CODE = "wurfl-devicedetection-entrypoint-hook"; + + @Override + public Future> call(EntrypointPayload entrypointPayload, + InvocationContext invocationContext) { + + final AuctionRequestHeadersContext bidRequestHeadersContext = AuctionRequestHeadersContext.from( + entrypointPayload.headers()); + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(bidRequestHeadersContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java new file mode 100644 index 00000000000..f929a33ce70 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.Collection; +import java.util.List; + +public class WURFLDeviceDetectionModule implements Module { + + public static final String CODE = "wurfl-devicedetection"; + + private final List> hooks; + + public WURFLDeviceDetectionModule(List> hooks) { + this.hooks = hooks; + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return this.hooks; + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java new file mode 100644 index 00000000000..5a92e051f0c --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java @@ -0,0 +1,133 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.Device; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.json.JacksonMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver.HeadersResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.settings.model.Account; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class WURFLDeviceDetectionRawAuctionRequestHook implements RawAuctionRequestHook { + + private static final Logger logger = LoggerFactory.getLogger(WURFLDeviceDetectionRawAuctionRequestHook.class); + + public static final String CODE = "wurfl-devicedetection-raw-auction-request"; + private static final String WURFL_PROPERTY = "wurfl"; + + private final WURFLService wurflService; + private final Set allowedPublisherIDs; + private final boolean addExtCaps; + private final JacksonMapper mapper; + + public WURFLDeviceDetectionRawAuctionRequestHook(WURFLService wurflService, + WURFLDeviceDetectionConfigProperties configProperties, + JacksonMapper mapper) { + + this.wurflService = Objects.requireNonNull(wurflService); + this.addExtCaps = Objects.requireNonNull(configProperties).isExtCaps(); + this.allowedPublisherIDs = Objects.requireNonNull(configProperties.getAllowedPublisherIds()); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + if (!shouldEnrichDevice(invocationContext)) { + return noActionResult(); + } + + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + final Device device = bidRequest.getDevice(); + if (device == null) { + logger.warn("Device is null"); + return noActionResult(); + } + + if (isDeviceAlreadyEnriched(device)) { + logger.info("Device is already enriched, returning original bid request"); + return noActionResult(); + } + + final Map requestHeaders = + invocationContext.moduleContext() instanceof AuctionRequestHeadersContext moduleContext + ? moduleContext.getHeaders() + : null; + + final Map headers = HeadersResolver.resolve(device, requestHeaders); + final Optional wurflDevice = wurflService.lookupDevice(headers); + if (wurflDevice.isEmpty()) { + logger.info("No WURFL device found, returning original bid request"); + return noActionResult(); + } + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(new OrtbDeviceUpdater( + wurflDevice.get(), + wurflService.getAllCapabilities(), + wurflService.getAllVirtualCapabilities(), + addExtCaps, + mapper)) + .build()); + } + + private boolean isDeviceAlreadyEnriched(Device device) { + final ExtDevice extDevice = device.getExt(); + if (extDevice != null && extDevice.containsProperty(WURFL_PROPERTY)) { + return true; + } + + // Check if other some of the other Device data are already set + final Integer deviceType = device.getDevicetype(); + final String hwv = device.getHwv(); + return deviceType != null && deviceType > 0 && StringUtils.isNotEmpty(hwv); + } + + private boolean shouldEnrichDevice(AuctionInvocationContext invocationContext) { + return CollectionUtils.isEmpty(allowedPublisherIDs) || isAccountValid(invocationContext.auctionContext()); + } + + private boolean isAccountValid(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getId) + .filter(StringUtils::isNotBlank) + .filter(allowedPublisherIDs::contains) + .isPresent(); + } + + private static Future> noActionResult() { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java new file mode 100644 index 00000000000..ccfc848399c --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLService.java @@ -0,0 +1,66 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.scientiamobile.wurfl.core.Device; +import com.scientiamobile.wurfl.core.WURFLEngine; +import io.vertx.core.Future; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineUtils; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class WURFLService implements FileProcessor { + + private static final Logger logger = LoggerFactory.getLogger(WURFLService.class); + + private final AtomicReference wurflEngine; + private final WURFLDeviceDetectionConfigProperties configProperties; + + public WURFLService(WURFLEngine wurflEngine, WURFLDeviceDetectionConfigProperties configProperties) { + this.wurflEngine = new AtomicReference<>(wurflEngine); + this.configProperties = Objects.requireNonNull(configProperties); + } + + public Future setDataPath(String dataFilePath) { + try { + final WURFLEngine engine = createEngine(dataFilePath); + this.wurflEngine.set(engine); + } catch (Exception e) { + return Future.failedFuture(e); + } + + return Future.succeededFuture(); + } + + protected WURFLEngine createEngine(String dataFilePath) { + final WURFLEngine wurflEngine = WURFLEngineUtils.initializeEngine(configProperties, dataFilePath); + wurflEngine.load(); + logger.info("WURFL Engine initialized"); + return wurflEngine; + } + + public Optional lookupDevice(Map headers) { + return Optional.ofNullable(wurflEngine.get()) + .map(engine -> engine.getDeviceForRequest(headers)); + } + + public Set getAllCapabilities() { + return Optional.ofNullable(wurflEngine.get()) + .map(WURFLEngine::getAllCapabilities) + .orElse(Collections.emptySet()); + } + + public Set getAllVirtualCapabilities() { + return Optional.ofNullable(wurflEngine.get()) + .map(WURFLEngine::getAllVirtualCapabilities) + .orElse(Collections.emptySet()); + } +} + diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java new file mode 100644 index 00000000000..c69a4ab503b --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import org.junit.jupiter.api.Test; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuctionRequestHeadersContextTest { + + @Test + public void fromShouldHandleNullHeaders() { + // when + final AuctionRequestHeadersContext result = AuctionRequestHeadersContext.from(null); + + // then + assertThat(result.getHeaders()).isEmpty(); + } + + @Test + public void fromShouldConvertCaseInsensitiveMultiMapToHeaders() { + // given + final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .add("Header2", "value2") + .build(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap); + + // then + assertThat(target.getHeaders()) + .hasSize(2) + .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .containsEntry("Header2", "value2"); + } + + @Test + public void fromShouldTakeFirstValueForDuplicateHeaders() { + // given + final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test2") + .build(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap); + + // then + assertThat(target.getHeaders()) + .hasSize(1) + .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test"); + } + + @Test + public void fromShouldHandleEmptyMultiMap() { + // given + final CaseInsensitiveMultiMap emptyMultiMap = CaseInsensitiveMultiMap.empty(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(emptyMultiMap); + + // then + assertThat(target.getHeaders()).isEmpty(); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java new file mode 100644 index 00000000000..a383f875768 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineUtilsTest.java @@ -0,0 +1,82 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; + +import static org.mockito.Mock.Strictness.LENIENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +public class WURFLEngineUtilsTest { + + @Mock(strictness = LENIENT) + private WURFLDeviceDetectionConfigProperties configProperties; + + @Test + public void extractWURFLFileNameShouldReturnCorrectFileName() { + // given + final String url = "https://data.examplehost.com/snapshot/wurfl-latest.zip"; + + // when + final String result = WURFLEngineUtils.extractWURFLFileName(url); + + // then + assertThat(result).isEqualTo("wurfl-latest.zip"); + } + + @Test + public void extractWURFLFileNameShouldHandleSimpleFileName() { + // given + final String url = "http://example.com/wurfl.zip"; + + // when + final String result = WURFLEngineUtils.extractWURFLFileName(url); + + // then + assertThat(result).isEqualTo("wurfl.zip"); + } + + @Test + public void extractWURFLFileNameShouldHandleComplexPath() { + // given + final String url = "https://examplehost.com/path/to/files/wurfl-snapshot.zip"; + + // when + final String result = WURFLEngineUtils.extractWURFLFileName(url); + + // then + assertThat(result).isEqualTo("wurfl-snapshot.zip"); + } + + @Test + public void extractWURFLFileNameShouldHandleUrlWithQueryParams() { + // given + final String url = "https://example.com/wurfl.zip?version=latest&format=zip"; + + // when + final String result = WURFLEngineUtils.extractWURFLFileName(url); + + // then + assertThat(result).isEqualTo("wurfl.zip"); + } + + @Test + public void extractWURFLFileNameShouldThrowExceptionForNullUrl() { + // when & then + assertThatThrownBy(() -> WURFLEngineUtils.extractWURFLFileName(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid WURFL snapshot URL: null"); + } + + @Test + public void initializeEngineShouldThrowExceptionForNullDataFilePath() { + // when & then + assertThatThrownBy(() -> WURFLEngineUtils.initializeEngine(configProperties, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid WURFL snapshot URL: null"); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java new file mode 100644 index 00000000000..ca45c27bf47 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java @@ -0,0 +1,172 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.UserAgent; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HeadersResolverTest { + + private static final String TEST_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"; + + @Test + public void resolveWithNullDeviceShouldReturnOriginalHeaders() { + // given + final Map headers = new HashMap<>(); + headers.put("test", "value"); + + // when + final Map result = HeadersResolver.resolve(null, headers); + + // then + assertThat(result).isEqualTo(headers); + } + + @Test + public void resolveWithDeviceUaShouldReturnUserAgentHeader() { + // given + final Device device = Device.builder() + .ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .build(); + + // when + final Map result = HeadersResolver.resolve(device, new HashMap<>()); + + // then + assertThat(result).containsEntry("User-Agent", TEST_USER_AGENT); + } + + @Test + public void resolveWithFullSuaShouldReturnAllHeaders() { + // given + final BrandVersion brandVersion = new BrandVersion( + "Chrome", + Arrays.asList("100", "0", "0"), + null); + + final BrandVersion winBrandVersion = new BrandVersion( + "Windows", + Arrays.asList("10", "0", "0"), + null); + final UserAgent sua = UserAgent.builder() + .browsers(List.of(brandVersion)) + .platform(winBrandVersion) + .model("Test Model") + .architecture("x86") + .mobile(0) + .build(); + + final Device device = Device.builder() + .sua(sua) + .build(); + + // when + final Map result = HeadersResolver.resolve(device, new HashMap<>()); + + // then + assertThat(result) + .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Platform", "Windows") + .containsEntry("Sec-CH-UA-Platform-Version", "10.0.0") + .containsEntry("Sec-CH-UA-Model", "Test Model") + .containsEntry("Sec-CH-UA-Arch", "x86") + .containsEntry("Sec-CH-UA-Mobile", "?0"); + } + + @Test + public void resolveWithFullDeviceAndHeadersShouldPrioritizeDevice() { + // given + final BrandVersion brandVersion = new BrandVersion( + "Chrome", + Arrays.asList("100", "0", "0"), + null); + + final BrandVersion winBrandVersion = new BrandVersion( + "Windows", + Arrays.asList("10", "0", "0"), + null); + final UserAgent sua = UserAgent.builder() + .browsers(List.of(brandVersion)) + .platform(winBrandVersion) + .model("Test Model") + .architecture("x86") + .mobile(0) + .build(); + + final Device device = Device.builder() + .sua(sua) + .ua(TEST_USER_AGENT) + .build(); + + final Map headers = new HashMap<>(); + headers.put("Sec-CH-UA", "Test UA-CH"); + headers.put("Sec-CH-UA-Full-Version-List", "Test-UA-Full-Version-List"); + headers.put("Sec-CH-UA-Platform", "Test-UA-Platform"); + headers.put("Sec-CH-UA-Platform-Version", "Test-UA-Platform-Version"); + headers.put("Sec-CH-UA-Model", "Test-UA-Model"); + headers.put("Sec-CH-UA-Arch", "Test-UA-Arch"); + headers.put("Sec-CH-UA-Mobile", "Test-UA-Mobile"); + headers.put("User-Agent", "Mozilla/5.0 (Test OS; 10) like Gecko"); + // when + final Map result = HeadersResolver.resolve(device, headers); + + // then + assertThat(result) + .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Platform", "Windows") + .containsEntry("Sec-CH-UA-Platform-Version", "10.0.0") + .containsEntry("Sec-CH-UA-Model", "Test Model") + .containsEntry("Sec-CH-UA-Arch", "x86") + .containsEntry("Sec-CH-UA-Mobile", "?0"); + } + + @Test + public void resolveWithMultipleBrandVersionsShouldFormatCorrectly() { + // given + final BrandVersion chrome = new BrandVersion("Chrome", + Arrays.asList("100", "0"), + null); + final BrandVersion chromium = new BrandVersion("Chromium", + Arrays.asList("100", "0"), + null); + + final BrandVersion notABrand = new BrandVersion("Not\\A;Brand", + Arrays.asList("99", "0"), + null); + + final UserAgent sua = UserAgent.builder() + .browsers(Arrays.asList(chrome, chromium, notABrand)) + .build(); + + final Device device = Device.builder() + .sua(sua) + .build(); + + // when + final Map result = HeadersResolver.resolve(device, new HashMap<>()); + + // then + final String expectedFormat = "\"Chrome\";v=\"100.0\", \"Chromium\";v=\"100.0\", \"Not\\A;Brand\";v=\"99.0\""; + assertThat(result) + .containsEntry("Sec-CH-UA", expectedFormat) + .containsEntry("Sec-CH-UA-Full-Version-List", expectedFormat); + } + + @Test + public void resolveWithNullDeviceAndNullHeadersShouldReturnEmptyMap() { + // when + final Map result = HeadersResolver.resolve(null, null); + + // then + assertThat(result).isEmpty(); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java new file mode 100644 index 00000000000..43300b0ff92 --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java @@ -0,0 +1,394 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; +import org.mockito.Mock; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import com.iab.openrtb.request.BidRequest; + +import java.math.BigDecimal; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OrtbDeviceUpdaterTest { + + private Set staticCaps; + private Set virtualCaps; + private JacksonMapper mapper; + + @Mock(strictness = LENIENT) + private AuctionRequestPayload payload; + + @Mock(strictness = LENIENT) + private com.scientiamobile.wurfl.core.Device wurflDevice; + + @BeforeEach + void setUp() { + staticCaps = Set.of("ajax_support_javascript", "brand_name", "density_class", + "is_connected_tv", "is_ott", "is_tablet", "model_name", "resolution_height", "resolution_width", + "physical_form_factor"); + virtualCaps = Set.of("advertised_device_os", "advertised_device_os_version", + "is_full_desktop", "pixel_density"); + mapper = new JacksonMapper(ObjectMapperProvider.mapper()); + } + + @Test + public void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() { + // given + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); + given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getMake()).isEqualTo("TestPhone"); + assertThat(resultDevice.getDevicetype()).isEqualTo(4); + } + + @Test + public void updateShouldNotUpdateDeviceMakeWhenOriginalExists() { + // given + final Device device = Device.builder().make("Samphone").model("youPhone").build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getMake()).isEqualTo("Samphone"); + assertThat(resultDevice.getModel()).isEqualTo("youPhone"); + } + + @Test + public void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() { + // given + final Device device = Device.builder().make("TestPhone").pxratio(new BigDecimal("1.0")).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("density_class")).willReturn("2.2"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getMake()).isEqualTo("TestPhone"); + assertThat(resultDevice.getPxratio()).isEqualTo("1.0"); + } + + @Test + public void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getModel()).isEqualTo("youPhone"); + } + + @Test + public void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("testOS"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getOs()).isEqualTo("testOS"); + } + + @Test + public void updateShouldUpdateResolutionWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + given(wurflDevice.getCapabilityAsInt("resolution_width")).willReturn(3200); + given(wurflDevice.getCapabilityAsInt("resolution_height")).willReturn(1440); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getW()).isEqualTo(3200); + assertThat(resultDevice.getH()).isEqualTo(1440); + } + + @Test + public void updateShouldHandleJavascriptSupport() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true); + given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getJs()).isEqualTo(1); + } + + @Test + public void updateShouldHandleOttDeviceType() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapabilityAsBool("is_ott")).willReturn(true); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(7); + } + + @Test + public void updateShouldHandleOutOfHomeDeviceType() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapability("physical_form_factor")).willReturn("out_of_home_device"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(8); + } + + @Test + public void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other Non-Mobile"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(6); + } + + @Test + public void updateShouldReturnNullDeviceTypeWhenMobileTypeIsUnknown() { + // given + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other non-Mobile"); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isNull(); + } + + @Test + public void updateShouldReturnNullDeviceTypeWhenFormFactorIsMissing() { + // given + given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); + given(wurflDevice.getVirtualCapability("is_phone")).willReturn("false"); + given(wurflDevice.getCapability("is_tablet")).willReturn("false"); + when(wurflDevice.getVirtualCapability("form_factor")) + .thenThrow(new VirtualCapabilityNotDefinedException("form_factor")); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isNull(); + } + + @Test + public void updateShouldReturnGenericMobileTabletDeviceTypeWhenFormFactorIsOtherMobile() { + // given + given(wurflDevice.getCapability("is_tablet")).willReturn("false"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other Mobile"); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(1); + } + + @Test + public void updateShouldHandlePersonalComputerDeviceType() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Desktop"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(2); + } + + @Test + public void updateShouldHandleConnectedTvDeviceType() { + // given + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smart-TV"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(3); + } + + @Test + public void updateShouldNotUpdateDeviceTypeWhenSet() { + // given + final Device device = Device.builder() + .devicetype(3) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(3); // unchanged + } + + @Test + public void updateShouldHandleTabletDeviceType() { + // given + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Tablet"); + + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(5); + } + + @Test + public void updateShouldHandlePhoneDeviceType() { + // given + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(4); + } + + @Test + public void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingProperties() { + // given + final ExtDevice existingExt = ExtDevice.empty(); + existingExt.addProperty("someProperty", TextNode.valueOf("value")); + final Device device = Device.builder() + .ext(existingExt) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); + given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true"); + given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); + given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true"); + given(wurflDevice.getCapability("is_tablet")).willReturn("false"); + given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + final Set staticCaps = Set.of("brand_name"); + final Set virtualCaps = Set.of("advertised_device_os"); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + final ExtDevice resultExt = resultDevice.getExt(); + assertThat(resultExt).isNotNull(); + assertThat(resultExt.getProperty("someProperty").textValue()).isEqualTo("value"); + assertThat(resultExt.getProperty("wurfl")).isNotNull(); + + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java new file mode 100644 index 00000000000..49131c4131f --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import io.vertx.core.Future; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class WURFLDeviceDetectionEntrypointHookTest { + + @Mock + private EntrypointPayload payload; + + @Mock + private InvocationContext context; + + @Test + public void codeShouldReturnCorrectHookCode() { + + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection-entrypoint-hook"); + } + + @Test + public void callShouldReturnSuccessWithNoAction() { + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .build(); + given(payload.headers()).willReturn(headers); + + // when + final Future> result = target.call(payload, context); + + // then + assertThat(result).isNotNull(); + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.moduleContext()).isNotNull(); + } + + @Test + public void callShouldHandleNullHeaders() { + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + + // when + given(payload.headers()).willReturn(null); + final Future> result = target.call(payload, context); + + // then + assertThat(result).isNotNull(); + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.moduleContext()).isNotNull(); + assertThat(invocationResult.moduleContext() instanceof AuctionRequestHeadersContext).isTrue(); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java new file mode 100644 index 00000000000..99ae360e5ac --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WURFLDeviceDetectionModuleTest { + + @Test + public void codeShouldReturnCorrectModuleCode() { + // given + final List> hooks = new ArrayList<>(); + final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks); + + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection"); + } + + @Test + public void hooksShouldReturnProvidedHooks() { + // given + final List> hooks = new ArrayList<>(); + final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks); + + // when + final Collection> result = target.hooks(); + + // then + assertThat(result).isSameAs(hooks); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java new file mode 100644 index 00000000000..f981f43ccaa --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java @@ -0,0 +1,300 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.WURFLEngine; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.settings.model.Account; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.ArgumentMatchers.any; + +@ExtendWith(MockitoExtension.class) +public class WURFLDeviceDetectionRawAuctionRequestHookTest { + + @Mock + private WURFLEngine wurflEngine; + + @Mock + private WURFLDeviceDetectionConfigProperties configProperties; + + @Mock + private AuctionRequestPayload payload; + + @Mock + private AuctionInvocationContext context; + + private AuctionContext auctionContext; + + @Mock(strictness = Mock.Strictness.LENIENT) + private Account account; + + @Mock(strictness = Mock.Strictness.LENIENT) + private com.scientiamobile.wurfl.core.Device wurflDevice; + + private JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); + + private WURFLDeviceDetectionRawAuctionRequestHook target; + + @BeforeEach + public void setUp() { + auctionContext = AuctionContext.builder().account(account).build(); + + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + } + + @Test + public void codeShouldReturnCorrectHookCode() { + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection-raw-auction-request"); + } + + @Test + public void callShouldReturnNoActionWhenDeviceIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + given(payload.bidRequest()).willReturn(bidRequest); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnNoActionWhenDeviceHasWurflProperty() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + final ExtDevicePrebid extDevicePrebid = ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)); + final ExtDevice extDevice = ExtDevice.of(0, extDevicePrebid); + final ObjectNode wurfl = mapper.mapper().createObjectNode(); + wurfl.put("wurfl_id", "test_phone_ver1"); + extDevice.addProperty("wurfl", wurfl); + final Device device = Device.builder() + .ua(ua) + .ext(extDevice) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnNoActionWhenDeviceHasDeviceTypeAndHwv() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + final Device device = Device.builder() + .ua(ua) + .hwv("test_phone_ver1") + .devicetype(1) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + given(wurflDevice.getId()).willReturn("test_phone_ver1"); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnActionUpdateWhenDeviceHasJustDeviceType() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + final Device device = Device.builder() + .ua(ua) + .devicetype(1) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + given(context.moduleContext()).willReturn(headersContext); + given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice); + given(wurflDevice.getId()).willReturn("test_phone_ver1"); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + + @Test + public void callShouldUpdateDeviceWhenWurflDeviceIsDetected() throws Exception { + // given + final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1"; + final Device device = Device.builder().ua(ua).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + given(context.moduleContext()).willReturn(headersContext); + given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice); + given(wurflDevice.getId()).willReturn("apple_iphone_ver1"); + given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); + given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + + @Test + public void shouldEnrichDeviceWhenAllowedPublisherIdsIsEmpty() throws Exception { + // given + final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1"; + final Device device = Device.builder().ua(ua).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + given(context.moduleContext()).willReturn(headersContext); + given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice); + given(wurflDevice.getId()).willReturn("apple_iphone_ver1"); + given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); + given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + + @Test + public void shouldEnrichDeviceWhenAccountIsAllowed() throws Exception { + // given + final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1"; + final Device device = Device.builder().ua(ua).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + given(context.moduleContext()).willReturn(headersContext); + given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice); + given(wurflDevice.getId()).willReturn("apple_iphone_ver1"); + given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); + given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); + given(account.getId()).willReturn("allowed-publisher"); + given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher", + "another-allowed-publisher")); + given(context.auctionContext()).willReturn(auctionContext); + + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + + @Test + public void shouldNotEnrichDeviceWhenPublisherIdIsNotAllowed() { + // given + given(context.auctionContext()).willReturn(auctionContext); + given(account.getId()).willReturn("unknown-publisher"); + given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher")); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void shouldNotEnrichDeviceWhenPublisherIdIsEmpty() { + // given + given(context.auctionContext()).willReturn(auctionContext); + given(account.getId()).willReturn(""); + given(configProperties.getAllowedPublisherIds()).willReturn(Set.of("allowed-publisher")); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } +} diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java new file mode 100644 index 00000000000..4dd2cfde2fe --- /dev/null +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLServiceTest.java @@ -0,0 +1,164 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.scientiamobile.wurfl.core.Device; +import com.scientiamobile.wurfl.core.WURFLEngine; +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.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(MockitoExtension.class) +public class WURFLServiceTest { + + @Mock(strictness = LENIENT) + private WURFLEngine wurflEngine; + + @Mock(strictness = LENIENT) + private WURFLDeviceDetectionConfigProperties configProperties; + + private WURFLService wurflService; + + @BeforeEach + public void setUp() { + wurflService = new WURFLService(wurflEngine, configProperties); + } + + @Test + public void setDataPathShouldReturnSucceededFutureWhenProcessingSucceeds() throws Exception { + // given + final String dataFilePath = "test-data-path"; + final String wurflSnapshotUrl = "http://example.com/wurfl-snapshot.zip"; + final String wurflFileDirPath = System.getProperty("java.io.tmpdir"); + + given(configProperties.getFileSnapshotUrl()).willReturn(wurflSnapshotUrl); + given(configProperties.getFileDirPath()).willReturn(wurflFileDirPath); + + final WURFLService spyWurflService = spy(wurflService); + doReturn(wurflEngine).when(spyWurflService).createEngine(dataFilePath); + + // when + final Future future = spyWurflService.setDataPath(dataFilePath); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + } + + @Test + public void setDataPathShouldReturnFailedFutureWhenExceptionOccurs() { + // given + final String dataFilePath = "test-data-path"; + final WURFLService spyWurflService = spy(wurflService); + doThrow(new RuntimeException("Simulated load() failure")) + .when(spyWurflService).createEngine(dataFilePath); + + // when + final Future result = spyWurflService.setDataPath(dataFilePath); + + // then + assertThat(result).isNotNull(); + assertThat(result.failed()).isTrue(); + assertThat(result.cause()).isInstanceOf(RuntimeException.class) + .hasMessage("Simulated load() failure"); + } + + @Test + public void lookupDeviceShouldReturnDeviceWhenEngineIsNotNull() { + // given + final Map headers = new HashMap<>(); + headers.put("User-Agent", "test-user-agent"); + + final Device expectedDevice = mock(Device.class); + given(wurflEngine.getDeviceForRequest(headers)).willReturn(expectedDevice); + + // when + final Optional result = wurflService.lookupDevice(headers); + + // then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(expectedDevice); + verify(wurflEngine).getDeviceForRequest(headers); + } + + @Test + public void lookupDeviceShouldReturnEmptyWhenEngineIsNull() { + // given + wurflService = new WURFLService(null, configProperties); + final Map headers = new HashMap<>(); + + // when + final Optional result = wurflService.lookupDevice(headers); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void getAllCapabilitiesShouldReturnCapabilitiesWhenEngineIsNotNull() { + // given + final Set expectedCapabilities = Set.of("capability1", "capability2"); + given(wurflEngine.getAllCapabilities()).willReturn(expectedCapabilities); + + // when + final Set result = wurflService.getAllCapabilities(); + + // then + assertThat(result).isEqualTo(expectedCapabilities); + verify(wurflEngine).getAllCapabilities(); + } + + @Test + public void getAllCapabilitiesShouldReturnEmptySetWhenEngineIsNull() { + // given + wurflService = new WURFLService(null, configProperties); + + // when + final Set result = wurflService.getAllCapabilities(); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void getAllVirtualCapabilitiesShouldReturnCapabilitiesWhenEngineIsNotNull() { + // given + final Set expectedCapabilities = Set.of("virtualCapability1", "virtualCapability2"); + given(wurflEngine.getAllVirtualCapabilities()).willReturn(expectedCapabilities); + + // when + final Set result = wurflService.getAllVirtualCapabilities(); + + // then + assertThat(result).isEqualTo(expectedCapabilities); + verify(wurflEngine).getAllVirtualCapabilities(); + } + + @Test + public void getAllVirtualCapabilitiesShouldReturnEmptySetWhenEngineIsNull() { + // given + wurflService = new WURFLService(null, configProperties); + + // when + final Set result = wurflService.getAllVirtualCapabilities(); + + // then + assertThat(result).isEmpty(); + } +} diff --git a/extra/pom.xml b/extra/pom.xml index dd0ac70cacc..836df8f4a38 100644 --- a/extra/pom.xml +++ b/extra/pom.xml @@ -4,7 +4,7 @@ org.prebid prebid-server-aggregator - 2.13.0-SNAPSHOT + 3.39.0-SNAPSHOT pom @@ -15,12 +15,60 @@ + + 21 UTF-8 - UTF-8 - 17 - 17 - 1.18.24 - 3.0.0-M6 + UTF-8 + ${java.version} + ${java.version} + + + 3.1.1 + 3.14.1 + 3.5.3 + ${maven-surefire-plugin.version} + 0.8.13 + 0.46.0 + 3.6.0 + 10.17.0 + + + 3.5.5 + 4.5.20 + 2.0.1.Final + 4.4 + 1.27.1 + 3.6.1 + 1.10.0 + 2.1 + 4.5.14 + 5.5.1 + 6.8.0 + 1.5.6 + 1.13 + 2.2.0 + + 1.2.3 + 0.16.0 + 2.0.10 + 3.2.4 + 4.2.1 + 3.25.6 + ${protobuf.version} + 1.0.9 + 2.31.22 + 4.2.30 + + + 3.12.1 + 2.4-M6-groovy-4.0 + + 5.15.0 + + + false + false + true @@ -29,26 +77,296 @@ bundle + + + + io.vertx + vertx-dependencies + ${vertx.version} + pom + import + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + io.dropwizard.metrics + metrics-bom + ${dropwizard-metrics.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} + + + commons-validator + commons-validator + ${commons-validator.version} + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + commons-logging + commons-logging + + + + + 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 + + + commons-logging + commons-logging + + + + + + org.projectlombok lombok - ${lombok.version} provided + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-plugin.version} + + + ${maven.multiModuleProjectDirectory}/checkstyle.xml + true + + true + false + true + + + + validate + + check + + + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + org.apache.maven.plugins 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 + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-plugin.version} + + + + checkstyle-aggregate + + + + + + diff --git a/mvnw b/mvnw index b7f064624f8..19529ddf8c6 100755 --- a/mvnw +++ b/mvnw @@ -19,269 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.1.1 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi fi else - JAVACMD="`\\unset -f command; \\command -v java`" + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi +} -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi +die() { + printf %s\\n "$1" >&2 + exit 1 +} - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` - fi - # end of workaround - done - printf '%s' "$(cd "$basedir"; pwd)" +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' } -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } -BASE_DIR=$(find_maven_basedir "$(dirname $0)") -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; - esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $wrapperUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" - if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` - fi + die "cannot create temp dir" +fi - if command -v wget > /dev/null; then - QUIET="--quiet" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - QUIET="" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" - fi - [ $? -eq 0 ] || rm -f "$wrapperJarPath" - elif command -v curl > /dev/null; then - QUIET="--silent" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - QUIET="" - fi - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L - fi - [ $? -eq 0 ] || rm -f "$wrapperJarPath" - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=`cygpath --path --windows "$javaSource"` - javaClass=`cygpath --path --windows "$javaClass"` - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") - fi - fi - fi +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -########################################################################################## -# End of extension -########################################################################################## -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" -export MAVEN_CMD_LINE_ARGS +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index cba1f040dc3..b150b91ed50 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -18,170 +19,131 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.1.1 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -@REM End of extension - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 4cca09abf73..b9f4a2eff8b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.prebid prebid-server-aggregator - 2.13.0-SNAPSHOT + 3.39.0-SNAPSHOT extra/pom.xml @@ -19,131 +19,30 @@ - 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 + 2.0.0 + 9.0.1 + + 4.2.1 + 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 +51,131 @@ 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} + + + commons-validator + commons-validator 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 + test org.postgresql postgresql - ${postgresql.version} + test + + + software.amazon.awssdk + s3 com.github.ben-manes.caffeine @@ -288,13 +184,6 @@ de.malkusch.whois-server-list public-suffix-list - ${psl.version} - - - com.google.code.findbugs - jsr305 - - io.dropwizard.metrics @@ -311,31 +200,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 +216,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 +261,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 +313,52 @@ 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.testcontainers + localstack + test + + + org.testcontainers + postgresql + test + + + org.testcontainers + influxdb test org.mock-server mockserver-client-java - ${mockserver.version} test - io.qameta.allure - allure-java-commons - ${allure.version} + org.influxdb + influxdb-java + 2.23 test @@ -603,6 +371,11 @@ org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} + + + -parameters + + org.apache.maven.plugins @@ -639,28 +412,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - ${checkstyle-plugin.version} - - - checkstyle.xml - UTF-8 - true - - true - false - true - - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - org.codehaus.gmavenplus gmavenplus-plugin @@ -696,18 +447,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - - validate - - checkstyle - - - - org.xolstice.maven.plugins protobuf-maven-plugin @@ -727,11 +466,13 @@ ${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} + - com.googlecode.maven-download-plugin + io.github.download-maven-plugin download-maven-plugin ${download-plugin.version} @@ -742,7 +483,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 +497,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,54 +521,23 @@ 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 - git-commit-id-plugin - ${git-commmit-plugin.version} + io.github.git-commit-id + git-commit-id-maven-plugin + ${git-commit-id-plugin.version} get-the-git-infos @@ -908,14 +622,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,57 +658,18 @@ - - - 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} - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - checkstyle - - - - - - 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/configs/localdev-config.yaml b/sample/configs/localdev-config.yaml new file mode 100644 index 00000000000..c2991cdf3b6 --- /dev/null +++ b/sample/configs/localdev-config.yaml @@ -0,0 +1,26 @@ +status-response: "ok" +adapters: + generic: + enabled: true + endpoint: http://localhost +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + filesystem: + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample/stored + stored-imps-dir: sample/stored + profiles-dir: sample/profiles + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 diff --git a/sample/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/configs/prebid-config-with-51d-dd.yaml b/sample/configs/prebid-config-with-51d-dd.yaml new file mode 100644 index 00000000000..d523bdbdf11 --- /dev/null +++ b/sample/configs/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/configs/sample-app-settings.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + 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/prebid-config-with-module.yaml b/sample/configs/prebid-config-with-module.yaml similarity index 91% rename from sample/prebid-config-with-module.yaml rename to sample/configs/prebid-config-with-module.yaml index 93a6a13d29f..06fc01fe3e2 100644 --- a/sample/prebid-config-with-module.yaml +++ b/sample/configs/prebid-config-with-module.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/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml new file mode 100644 index 00000000000..d9e736e96ac --- /dev/null +++ b/sample/configs/prebid-config-with-optable.yaml @@ -0,0 +1,53 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true + improvedigital: + enabled: true + colossus: + enabled: true + triplelift: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings-optable.yaml + stored-requests-dir: sample + stored-imps-dir: sample + 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: + optable-targeting: + enabled: true + modules: + optable-targeting: + api-endpoint: https://na.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}} diff --git a/sample/configs/prebid-config-with-wurfl.yaml b/sample/configs/prebid-config-with-wurfl.yaml new file mode 100644 index 00000000000..0ce8abebe17 --- /dev/null +++ b/sample/configs/prebid-config-with-wurfl.yaml @@ -0,0 +1,91 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample + profiles-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + wurfl-devicedetection: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-raw-auction-request" + } + ] + } + ] + } + } + } + } + } + + + modules: + wurfl-devicedetection: + file-dir-path: + # replace with your wurfl file snapshot URL when using a licensed version of wurfl + file-snapshot-url: https://httpstat.us/200 + cache-size: 200000 + update-frequency-in-hours: 24 + allowed-publisher-ids: 1 + ext-caps: true diff --git a/sample/prebid-config.yaml b/sample/configs/prebid-config.yaml similarity index 78% rename from sample/prebid-config.yaml rename to sample/configs/prebid-config.yaml index dc5921bc41e..0d8b5c90ff2 100644 --- a/sample/prebid-config.yaml +++ b/sample/configs/prebid-config.yaml @@ -21,10 +21,11 @@ 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 + profiles-dir: sample + stored-responses-dir: sample categories-dir: gdpr: default-value: 1 diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml new file mode 100644 index 00000000000..7a533da3697 --- /dev/null +++ b/sample/configs/sample-app-settings-optable.yaml @@ -0,0 +1,63 @@ +accounts: + - id: 1 + status: active + auction: + price-granularity: low + privacy: + ccpa: + enabled: true + gdpr: + enabled: true + cookie-sync: + default-limit: 8 + max-limit: 15 + coop-sync: + default: true + analytics: + allow-client-details: true + hooks: + modules: + optable-targeting: + api-key: key + tenant: optable + origin: web-sdk-demo + ppid-mapping: { "pubcid.org": "c" } + adserver-targeting: true + cache: + enabled: false + ttlseconds: 86400 + execution-plan: + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 600, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } 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/profiles/README.md b/sample/profiles/README.md new file mode 100644 index 00000000000..ff7afdd219b --- /dev/null +++ b/sample/profiles/README.md @@ -0,0 +1,2 @@ +### This is a directory to store profiles for file-based configuration. +### Please put only valid OpenRTB JSON files here. diff --git a/sample/requests/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/localdev-test-request.http b/sample/requests/localdev-test-request.http new file mode 100644 index 00000000000..eb5da3d7b35 --- /dev/null +++ b/sample/requests/localdev-test-request.http @@ -0,0 +1,36 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "generic": {} + } + } + } + } + ], + "test": 1 +} + +### diff --git a/sample/requests/localdev-test-stored-auction-response.http b/sample/requests/localdev-test-stored-auction-response.http new file mode 100644 index 00000000000..45509e61570 --- /dev/null +++ b/sample/requests/localdev-test-stored-auction-response.http @@ -0,0 +1,36 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + } + } + ], + "ext": { + "prebid": { + "storedauctionresponse": { + "id": "sample-stored-auction-response" + } + } + }, + "test": 1 +} + +### diff --git a/sample/requests/localdev-test-stored-bidder-response.http b/sample/requests/localdev-test-stored-bidder-response.http new file mode 100644 index 00000000000..eec448271e5 --- /dev/null +++ b/sample/requests/localdev-test-stored-bidder-response.http @@ -0,0 +1,42 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "generic": {} + }, + "storedbidresponse": [ + { + "bidder": "generic", + "id": "sample-stored-bidder-response" + } + ] + } + } + } + ], + "test": 1 +} + +### diff --git a/sample/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/sample/stored/optable-stored-response.json b/sample/stored/optable-stored-response.json new file mode 100644 index 00000000000..66c6a86b13b --- /dev/null +++ b/sample/stored/optable-stored-response.json @@ -0,0 +1,166 @@ +[ + { + "bid": + [ + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "12323-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.50", + "hb_size": "300x450" + }, + "type": "banner" + } + }, + "h": 450, + "id": "a961e06a-cee0-4160-9894-7a5ce7823c23", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.50, + "w": 300 + }, + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "50a4bd72-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.00", + "hb_size": "300x250" + }, + "type": "banner" + } + }, + "h": 250, + "id": "7fec12e0-e1d2-428f-b43c-0c2283843586", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.00, + "w": 300 + } + ], + "seat": "bidder1" + }, + { + "bid": + [ + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "12323-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.50", + "hb_size": "300x450" + }, + "type": "banner" + } + }, + "h": 450, + "id": "ba65ff9c-25f5-41ff-8c1d-21443479d7b9", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.50, + "w": 300 + }, + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "50a4bd72-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.00", + "hb_size": "300x250" + }, + "type": "banner" + } + }, + "h": 250, + "id": "374b5f85-e0b9-4f25-98ca-863c0698005a", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.00, + "w": 300 + } + ], + "seat": "bidder2" + } +] diff --git a/sample/stored/sample-stored-auction-response.json b/sample/stored/sample-stored-auction-response.json new file mode 100644 index 00000000000..34887d0fbb5 --- /dev/null +++ b/sample/stored/sample-stored-auction-response.json @@ -0,0 +1,14 @@ +{ + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
Ad Content
", + "crid": "creative123", + "w": 300, + "h": 250 + } + ], + "seat": "stored-auction-response-seat" +} diff --git a/sample/stored/sample-stored-bidder-response.json b/sample/stored/sample-stored-bidder-response.json new file mode 100644 index 00000000000..e1dba2b29d1 --- /dev/null +++ b/sample/stored/sample-stored-bidder-response.json @@ -0,0 +1,17 @@ +{ + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
Ad Content
", + "crid": "creative123", + "w": 300, + "h": 250 + } + ] + } + ] +} 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/Asset.java b/src/main/java/com/iab/openrtb/request/Asset.java index c60879c12bb..7be06cd7e03 100644 --- a/src/main/java/com/iab/openrtb/request/Asset.java +++ b/src/main/java/com/iab/openrtb/request/Asset.java @@ -8,7 +8,7 @@ @Value public class Asset { - public static final Asset EMPTY = com.iab.openrtb.request.Asset.builder().build(); + public static final Asset EMPTY = Asset.builder().build(); Integer id; diff --git a/src/main/java/com/iab/openrtb/request/Banner.java b/src/main/java/com/iab/openrtb/request/Banner.java index 0ec7796b394..b24584b110b 100644 --- a/src/main/java/com/iab/openrtb/request/Banner.java +++ b/src/main/java/com/iab/openrtb/request/Banner.java @@ -11,7 +11,7 @@ * “banner” may have very specific meaning in other contexts, here it can be * many things including a simple static image, an expandable ad unit, or even * in-banner video (refer to the {@link Video} object in Section 3.2.7 for the - * more generalized and full featured video ad units). An array of + * more generalized and full-featured video ad units). An array of * {@link Banner} objects can also appear within the {@link Video} to describe * optional companion ads defined in the VAST specification. *

The presence of a {@link Banner} as a subordinate of the {@link Imp} diff --git a/src/main/java/com/iab/openrtb/request/BidRequest.java b/src/main/java/com/iab/openrtb/request/BidRequest.java index f021cf5e1e9..79de78bb242 100644 --- a/src/main/java/com/iab/openrtb/request/BidRequest.java +++ b/src/main/java/com/iab/openrtb/request/BidRequest.java @@ -105,7 +105,7 @@ public class BidRequest { /** * Flag to indicate if Exchange can verify that the impressions offered - * represent all of the impressions available in context (e.g., all on the + * represent all the impressions available in context (e.g., all on the * web page, all video spots such as pre/mid/post roll) to support * road-blocking. 0 = no or unknown, 1 = yes, the impressions offered * represent all that are available. diff --git a/src/main/java/com/iab/openrtb/request/Channel.java b/src/main/java/com/iab/openrtb/request/Channel.java index a5a51c64055..84e0c7d2748 100644 --- a/src/main/java/com/iab/openrtb/request/Channel.java +++ b/src/main/java/com/iab/openrtb/request/Channel.java @@ -9,7 +9,7 @@ * {@link Channel} is defined as the entity that curates a content * library, or stream within a brand name for viewers. Examples are * specific view selectable ‘channels’ within linear and streaming - * television (MTV, HGTV, CNN, BBC One, etc) or a specific stream of + * television (MTV, HGTV, CNN, BBC One, etc.) or a specific stream of * audio content commonly called ‘stations.’ Name is a human-readable * field while domain and id can be used for reporting and targeting * purposes. See 7.6 for further examples. @@ -25,7 +25,7 @@ public class Channel { String id; /** - * Channel the content is on (e.g., a local channel like “WABC-TV") + * Channel the content is on (e.g., a local channel like "WABC-TV") */ String name; diff --git a/src/main/java/com/iab/openrtb/request/Content.java b/src/main/java/com/iab/openrtb/request/Content.java index 8ef947e810d..486630d8eed 100644 --- a/src/main/java/com/iab/openrtb/request/Content.java +++ b/src/main/java/com/iab/openrtb/request/Content.java @@ -9,7 +9,7 @@ /** * This object describes the content in which the impression will appear, which - * may be syndicated or non- syndicated content. This object may be useful when + * may be syndicated or non-syndicated content. This object may be useful when * syndicated content contains impressions and does not necessarily match the * publisher’s general content. The exchange might or might not have knowledge * of the page where the content is running, because of the syndication @@ -60,7 +60,7 @@ public class Content { String artist; /** - * Genre that best describes the content (e.g., rock, pop, etc). + * Genre that best describes the content (e.g., rock, pop, etc.). */ String genre; @@ -161,7 +161,7 @@ public class Content { String langb; /** - * Indicator of whether or not the content is embeddable (e.g., an + * Indicator of whether the content is embeddable (e.g., an * embeddable video player), where 0 = no, 1 = yes. */ Integer embeddable; diff --git a/src/main/java/com/iab/openrtb/request/Device.java b/src/main/java/com/iab/openrtb/request/Device.java index 453406e126a..848e1b01e66 100644 --- a/src/main/java/com/iab/openrtb/request/Device.java +++ b/src/main/java/com/iab/openrtb/request/Device.java @@ -10,7 +10,7 @@ * This object provides information pertaining to the device through which the * user is interacting. {@link Device} information includes its hardware, platform, * location, and carrier data. The device can refer to a mobile handset, - * a desktop computer, set top box, or other digital device. + * a desktop computer, set-top box, or other digital device. *

BEST PRACTICE: There are currently no prominent open source lists * for device makes, models, operating systems, or carriers. Exchanges typically * use commercial products or other proprietary lists for these attributes. @@ -18,7 +18,7 @@ * to publish lists of their device make, model, operating system, and carrier * values to bidders. *

BEST PRACTICE: Proper device IP detection in mobile is not - * straightforward. Typically it involves starting at the left of the + * straightforward. Typically, it involves starting at the left of the * {@code x-forwarded-for} header, skipping private carrier networks * (e.g., 10.x.x.x or 192.x.x.x), and possibly scanning for known carrier IP * ranges. Exchanges are urged to research and implement this feature carefully diff --git a/src/main/java/com/iab/openrtb/request/DurFloor.java b/src/main/java/com/iab/openrtb/request/DurFloor.java index 1c375372129..c03c5f657ee 100644 --- a/src/main/java/com/iab/openrtb/request/DurFloor.java +++ b/src/main/java/com/iab/openrtb/request/DurFloor.java @@ -28,7 +28,7 @@ public class DurFloor { /** * Minimum bid for a given impression opportunity, * if bidding with a creative in this duration range, expressed in CPM. - * For any creatives whose durations are outside of the defined min/max, + * For any creatives whose durations are outside the defined min/max, * the `bidfloor` at the `Imp` level will serve as the default floor. */ BigDecimal bidfloor; 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/Imp.java b/src/main/java/com/iab/openrtb/request/Imp.java index 98392688e2c..a24e90d464e 100644 --- a/src/main/java/com/iab/openrtb/request/Imp.java +++ b/src/main/java/com/iab/openrtb/request/Imp.java @@ -29,7 +29,7 @@ public class Imp { /** * A unique identifier for this impression within the context of the bid - * request (typically, starts with 1 and increments. + * request (typically, starts with 1 and increments). *

(required) */ String id; diff --git a/src/main/java/com/iab/openrtb/request/Native.java b/src/main/java/com/iab/openrtb/request/Native.java index 2739b3ee2f4..0c11132b12d 100644 --- a/src/main/java/com/iab/openrtb/request/Native.java +++ b/src/main/java/com/iab/openrtb/request/Native.java @@ -34,7 +34,7 @@ public class Native { /** * Request payload complying with the Native Ad Specification. The root node * of the payload, “native”, was dropped in the Native Ad Specification 1.1. - *

For Native 1.0, this is a JSON-encoded string consisting of a unnamed + *

For Native 1.0, this is a JSON-encoded string consisting of an unnamed * root object with a single subordinate object named 'native', which is the * Native Markup Request object, section 4.1 of OpenRTB Native 1.0 specification. *

For Native 1.1 and higher, this is a JSON-encoded string consisting of diff --git a/src/main/java/com/iab/openrtb/request/Network.java b/src/main/java/com/iab/openrtb/request/Network.java index 7d67627ab7e..8d3e3350f92 100644 --- a/src/main/java/com/iab/openrtb/request/Network.java +++ b/src/main/java/com/iab/openrtb/request/Network.java @@ -25,7 +25,7 @@ public class Network { String id; /** - * Network the content is on (e.g., a TV network like “ABC") + * Network the content is on (e.g., a TV network like "ABC") */ String name; diff --git a/src/main/java/com/iab/openrtb/request/Qty.java b/src/main/java/com/iab/openrtb/request/Qty.java index 191fab718d1..64e882de199 100644 --- a/src/main/java/com/iab/openrtb/request/Qty.java +++ b/src/main/java/com/iab/openrtb/request/Qty.java @@ -23,7 +23,7 @@ public class Qty { BigDecimal multiplier; /** - * The source type of the quantity measurement, ie. publisher. + * The source type of the quantity measurement, i.e. publisher. */ Integer sourcetype; diff --git a/src/main/java/com/iab/openrtb/request/Regs.java b/src/main/java/com/iab/openrtb/request/Regs.java index 932ae34784a..ffee5dd3216 100644 --- a/src/main/java/com/iab/openrtb/request/Regs.java +++ b/src/main/java/com/iab/openrtb/request/Regs.java @@ -23,7 +23,7 @@ public class Regs { Integer coppa; /** - * Flag that indicates whether or not the request is subject to + * Flag that indicates whether the request is subject to * GDPR regulations 0 = No, 1 = Yes, omission indicates Unknown. * Refer to Section 7.5 for more information. */ diff --git a/src/main/java/com/iab/openrtb/request/Request.java b/src/main/java/com/iab/openrtb/request/Request.java index 6f631fc152a..09ddac68d34 100644 --- a/src/main/java/com/iab/openrtb/request/Request.java +++ b/src/main/java/com/iab/openrtb/request/Request.java @@ -44,7 +44,7 @@ public class Request { /** Set to '0' in case if supply source / impression supports returning an assets url. */ Integer aurlsupport; - /** Set to '0' in case if supply source / impression supports returning an dco url. */ + /** Set to '0' in case if supply source / impression supports returning a dco url. */ Integer durlsupport; /** Specifies types of events supported by tracking. */ diff --git a/src/main/java/com/iab/openrtb/request/Source.java b/src/main/java/com/iab/openrtb/request/Source.java index e4804fea972..142a55fc7c4 100644 --- a/src/main/java/com/iab/openrtb/request/Source.java +++ b/src/main/java/com/iab/openrtb/request/Source.java @@ -36,8 +36,8 @@ public class Source { String pchain; /** - * This object represents both the links in the supply chain as - * well as an indicator whether or not the supply chain is complete. + * This object represents both the links in the supply chain and + * an indicator whether the supply chain is complete. * Details via the {@link SupplyChain} object (section 3.2.25) */ SupplyChain schain; diff --git a/src/main/java/com/iab/openrtb/request/SupplyChainNode.java b/src/main/java/com/iab/openrtb/request/SupplyChainNode.java index 1896cc7efea..ead56a2cdae 100644 --- a/src/main/java/com/iab/openrtb/request/SupplyChainNode.java +++ b/src/main/java/com/iab/openrtb/request/SupplyChainNode.java @@ -14,7 +14,7 @@ public class SupplyChainNode { /** * The canonical domain name of the SSP, Exchange, Header - * Wrapper, etc system that bidders connect to. This may be + * Wrapper, etc. system that bidders connect to. This may be * the operational domain of the system, if that is different than * the parent corporate domain, to facilitate WHOIS and * reverse IP lookups to establish clear ownership of the 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/com/iab/openrtb/request/ntv/ContextSubType.java b/src/main/java/com/iab/openrtb/request/ntv/ContextSubType.java index 82e9edb98e2..71392349d73 100644 --- a/src/main/java/com/iab/openrtb/request/ntv/ContextSubType.java +++ b/src/main/java/com/iab/openrtb/request/ntv/ContextSubType.java @@ -14,7 +14,7 @@ public enum ContextSubType { */ GENERAL(10), /** - * Primarily article content (which of course could include images, etc as part of the article) + * Primarily article content (which of course could include images, etc. as part of the article) */ ARTICLE(11), /** diff --git a/src/main/java/com/iab/openrtb/request/video/CacheConfig.java b/src/main/java/com/iab/openrtb/request/video/CacheConfig.java index f3c58da0dca..e02f683862b 100644 --- a/src/main/java/com/iab/openrtb/request/video/CacheConfig.java +++ b/src/main/java/com/iab/openrtb/request/video/CacheConfig.java @@ -1,12 +1,9 @@ package com.iab.openrtb.request.video; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheConfig { Integer ttl; } - diff --git a/src/main/java/com/iab/openrtb/request/video/IncludeBrandCategory.java b/src/main/java/com/iab/openrtb/request/video/IncludeBrandCategory.java index b0b8c28f56d..0b6a489694e 100644 --- a/src/main/java/com/iab/openrtb/request/video/IncludeBrandCategory.java +++ b/src/main/java/com/iab/openrtb/request/video/IncludeBrandCategory.java @@ -1,11 +1,9 @@ package com.iab.openrtb.request.video; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class IncludeBrandCategory { @JsonProperty("primaryadserver") @@ -16,4 +14,3 @@ public class IncludeBrandCategory { @JsonProperty("translatecategories") Boolean translateCategories; } - diff --git a/src/main/java/com/iab/openrtb/request/video/Pod.java b/src/main/java/com/iab/openrtb/request/video/Pod.java index a96e312f2ea..1f899dcbc18 100644 --- a/src/main/java/com/iab/openrtb/request/video/Pod.java +++ b/src/main/java/com/iab/openrtb/request/video/Pod.java @@ -1,11 +1,9 @@ package com.iab.openrtb.request.video; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class Pod { @JsonProperty("podid") @@ -17,4 +15,3 @@ public class Pod { @JsonProperty("configid") String configId; } - diff --git a/src/main/java/com/iab/openrtb/request/video/PodError.java b/src/main/java/com/iab/openrtb/request/video/PodError.java index a1219cfd595..2a1c1f8c09a 100644 --- a/src/main/java/com/iab/openrtb/request/video/PodError.java +++ b/src/main/java/com/iab/openrtb/request/video/PodError.java @@ -1,12 +1,10 @@ package com.iab.openrtb.request.video; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class PodError { Integer podId; @@ -15,4 +13,3 @@ public class PodError { List podErrors; } - diff --git a/src/main/java/com/iab/openrtb/request/video/Podconfig.java b/src/main/java/com/iab/openrtb/request/video/Podconfig.java index 35d5d52127f..229a939e4cc 100644 --- a/src/main/java/com/iab/openrtb/request/video/Podconfig.java +++ b/src/main/java/com/iab/openrtb/request/video/Podconfig.java @@ -18,4 +18,3 @@ public class Podconfig { List pods; } - diff --git a/src/main/java/com/iab/openrtb/response/Link.java b/src/main/java/com/iab/openrtb/response/Link.java index 8c8c3e576a7..57f740f00e9 100644 --- a/src/main/java/com/iab/openrtb/response/Link.java +++ b/src/main/java/com/iab/openrtb/response/Link.java @@ -1,7 +1,6 @@ package com.iab.openrtb.response; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -9,8 +8,7 @@ /** * Used for ‘call to action’ assets, or other links from the Native ad. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class Link { /** 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..007bf469d1a 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/ActivityInfrastructure.java @@ -2,13 +2,16 @@ 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.activity.infrastructure.privacy.PrivacyModuleQualifier; import org.prebid.server.proto.openrtb.ext.response.ExtTraceActivityInfrastructure; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; public class ActivityInfrastructure { @@ -40,7 +43,15 @@ 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(); } + + public Set skippedPrivacyModules() { + return debug.skippedPrivacyModules(); + } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java index 1651d1bc3a6..1f199646857 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/ActivityControllerCreationContext.java @@ -20,6 +20,8 @@ public class ActivityControllerCreationContext { Map privacyModulesConfigs; + Set skipPrivacyModules; + @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) Set usedPrivacyModules = EnumSet.noneOf(PrivacyModuleQualifier.class); 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..dff0f808435 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; @@ -23,14 +23,18 @@ import org.prebid.server.settings.model.Purposes; import org.prebid.server.settings.model.activity.AccountActivityConfiguration; import org.prebid.server.settings.model.activity.privacy.AccountPrivacyModuleConfig; +import org.prebid.server.settings.model.activity.rule.AccountActivityRuleConfig; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Supplier; @@ -41,6 +45,8 @@ public class ActivityInfrastructureCreator { private static final Logger logger = LoggerFactory.getLogger(ActivityInfrastructureCreator.class); + private static final int MODULE_MAX_SKIP_RATE = 100; + private final ActivityRuleFactory activityRuleFactory; private final Purpose defaultPurpose4; private final Metrics metrics; @@ -75,15 +81,22 @@ Map parse(Account account, GppContext gppContext, final Map activitiesConfiguration = accountPrivacyConfig .map(AccountPrivacyConfig::getActivities) .orElseGet(Collections::emptyMap); + final Map modulesConfigs = accountPrivacyConfig .map(AccountPrivacyConfig::getModules) .orElseGet(Collections::emptyList) .stream() + .filter(Objects::nonNull) .collect(Collectors.toMap( AccountPrivacyModuleConfig::getCode, UnaryOperator.identity(), takeFirstAndLogDuplicates(account.getId()))); + final Set skipPrivacyModules = modulesConfigs.entrySet().stream() + .filter(entry -> shouldSkipPrivacyModule(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(PrivacyModuleQualifier.class))); + return Arrays.stream(Activity.values()).collect(Collectors.toMap( UnaryOperator.identity(), fallbackActivity( @@ -93,6 +106,7 @@ Map parse(Account account, GppContext gppContext, activity, activitiesConfiguration.get(activity), modulesConfigs, + skipPrivacyModules, gppContext, debug)), (oldValue, newValue) -> oldValue, @@ -124,16 +138,21 @@ 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) : activityControllerCreator.apply(originalActivity); } + private static boolean shouldSkipPrivacyModule(AccountPrivacyModuleConfig config) { + return ThreadLocalRandom.current().nextInt(MODULE_MAX_SKIP_RATE) < config.getSkipRate(); + } + private ActivityController from(Activity activity, AccountActivityConfiguration activityConfiguration, Map modulesConfigs, + Set skipPrivacyModules, GppContext gppContext, ActivityInfrastructureDebug debug) { @@ -147,12 +166,14 @@ private ActivityController from(Activity activity, final ActivityControllerCreationContext creationContext = ActivityControllerCreationContext.of( activity, modulesConfigs, + skipPrivacyModules, gppContext); final boolean allow = allowFromConfig(activityConfiguration.getAllow()); final List rules = ListUtils.emptyIfNull(activityConfiguration.getRules()).stream() .filter(Objects::nonNull) - .map(ruleConfiguration -> activityRuleFactory.from(ruleConfiguration, creationContext)) + .map(ruleConfiguration -> createRule(ruleConfiguration, creationContext)) + .filter(Objects::nonNull) .toList(); return ActivityController.of(allow, rules, debug); @@ -162,6 +183,20 @@ private static boolean allowFromConfig(Boolean configValue) { return configValue != null ? configValue : ActivityInfrastructure.ALLOW_ACTIVITY_BY_DEFAULT; } + private Rule createRule(AccountActivityRuleConfig ruleConfiguration, + ActivityControllerCreationContext creationContext) { + + try { + return activityRuleFactory.from(ruleConfiguration, creationContext); + } catch (Exception e) { + logger.error("ActivityInfrastructure rule creation failed: %s. Configuration: %s" + .formatted(e.getMessage(), ruleConfiguration)); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } + } + private static Supplier> enumMapFactory() { return () -> new EnumMap<>(Activity.class); } 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..357eb916b5a 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 @@ -14,9 +14,10 @@ import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicDataSupplier; import org.prebid.server.activity.infrastructure.privacy.uscustomlogic.USCustomLogicModule; import org.prebid.server.auction.gpp.model.GppContext; -import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.json.DecodeException; import org.prebid.server.json.JsonLogic; +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.SettingsCache; @@ -34,6 +35,9 @@ public class USCustomLogicModuleCreator implements PrivacyModuleCreator { + private static final Logger logger = LoggerFactory.getLogger(USCustomLogicModuleCreator.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final Set ALLOWED_SECTIONS_IDS = PrivacySection.US_PRIVACY_SECTIONS.stream() .map(PrivacySection::sectionId) @@ -43,19 +47,22 @@ public class USCustomLogicModuleCreator implements PrivacyModuleCreator { private final JsonLogic jsonLogic; private final Map jsonLogicNodesCache; private final Metrics metrics; + private final double samplingRate; public USCustomLogicModuleCreator(USCustomLogicGppReaderFactory gppReaderFactory, JsonLogic jsonLogic, Integer cacheTtl, Integer cacheSize, - Metrics metrics) { + Metrics metrics, + double samplingRate) { this.gppReaderFactory = Objects.requireNonNull(gppReaderFactory); this.jsonLogic = Objects.requireNonNull(jsonLogic); this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; jsonLogicNodesCache = cacheTtl != null && cacheSize != null - ? SettingsCache.createCache(cacheTtl, cacheSize) + ? SettingsCache.createCache(cacheTtl, cacheSize, 0) : null; } @@ -76,6 +83,7 @@ public PrivacyModule from(PrivacyModuleCreationContext creationContext) { ? SetUtils.emptyIfNull(scope.getSectionsIds()).stream() .filter(sectionId -> shouldApplyPrivacy(sectionId, moduleConfig)) .map(sectionId -> forConfig(sectionId, normalizeSection, scope.getGppModel(), jsonLogicConfig)) + .filter(Objects::nonNull) .toList() : Collections.emptyList(); @@ -123,25 +131,25 @@ private PrivacyModule forConfig(int sectionId, GppModel gppModel, ObjectNode jsonLogicConfig) { - return new USCustomLogicModule( - jsonLogic, - jsonLogicNode(jsonLogicConfig), - USCustomLogicDataSupplier.of(gppReaderFactory.forSection(sectionId, normalizeSection, gppModel))); + try { + return new USCustomLogicModule( + jsonLogic, + jsonLogicNode(jsonLogicConfig), + USCustomLogicDataSupplier.of(gppReaderFactory.forSection(sectionId, normalizeSection, gppModel))); + } catch (Exception e) { + conditionalLogger.error( + "USCustomLogic creation failed: %s. Config: %s".formatted(e.getMessage(), jsonLogicConfig), + samplingRate); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } } private JsonLogicNode jsonLogicNode(ObjectNode jsonLogicConfig) { final String jsonAsString = jsonLogicConfig.toString(); return jsonLogicNodesCache != null - ? jsonLogicNodesCache.computeIfAbsent(jsonAsString, this::parseJsonLogicNode) - : parseJsonLogicNode(jsonAsString); - } - - private JsonLogicNode parseJsonLogicNode(String jsonLogicConfig) { - try { - return jsonLogic.parse(jsonLogicConfig); - } catch (DecodeException e) { - metrics.updateAlertsMetrics(MetricName.general); - throw new InvalidAccountConfigException("JsonLogic exception: " + e.getMessage()); - } + ? jsonLogicNodesCache.computeIfAbsent(jsonAsString, jsonLogic::parse) + : jsonLogic.parse(jsonAsString); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java index 012d913707d..f7d7f0fbfcd 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/privacy/usnat/USNatModuleCreator.java @@ -11,6 +11,11 @@ import org.prebid.server.activity.infrastructure.privacy.PrivacySection; import org.prebid.server.activity.infrastructure.privacy.usnat.USNatModule; import org.prebid.server.auction.gpp.model.GppContext; +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.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; @@ -20,15 +25,22 @@ public class USNatModuleCreator implements PrivacyModuleCreator { + private static final Logger logger = LoggerFactory.getLogger(USNatModuleCreator.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final Set ALLOWED_SECTIONS_IDS = PrivacySection.US_PRIVACY_SECTIONS.stream() .map(PrivacySection::sectionId) .collect(Collectors.toSet()); private final USNatGppReaderFactory gppReaderFactory; + private final Metrics metrics; + private final double samplingRate; - public USNatModuleCreator(USNatGppReaderFactory gppReaderFactory) { + public USNatModuleCreator(USNatGppReaderFactory gppReaderFactory, Metrics metrics, double samplingRate) { this.gppReaderFactory = Objects.requireNonNull(gppReaderFactory); + this.metrics = Objects.requireNonNull(metrics); + this.samplingRate = samplingRate; } @Override @@ -43,7 +55,12 @@ public PrivacyModule from(PrivacyModuleCreationContext creationContext) { final List innerPrivacyModules = SetUtils.emptyIfNull(scope.getSectionsIds()).stream() .filter(sectionId -> !shouldSkip(sectionId, moduleConfig)) - .map(sectionId -> forSection(creationContext.getActivity(), sectionId, scope.getGppModel())) + .map(sectionId -> forSection( + creationContext.getActivity(), + sectionId, + scope.getGppModel(), + moduleConfig.getConfig())) + .filter(Objects::nonNull) .toList(); return new AndPrivacyModules(innerPrivacyModules); @@ -61,7 +78,21 @@ private static boolean shouldSkip(Integer sectionId, AccountUSNatModuleConfig mo || (skipSectionIds != null && skipSectionIds.contains(sectionId)); } - private PrivacyModule forSection(Activity activity, Integer sectionId, GppModel gppModel) { - return new USNatModule(activity, gppReaderFactory.forSection(sectionId, gppModel)); + private PrivacyModule forSection(Activity activity, + Integer sectionId, + GppModel gppModel, + AccountUSNatModuleConfig.Config config) { + + try { + return new USNatModule(activity, gppReaderFactory.forSection(sectionId, gppModel), config); + } catch (Exception e) { + conditionalLogger.error( + "UsNat privacy module creation failed: %s. Activity: %s. Section: %s. Gpp: %s.".formatted( + e.getMessage(), activity, sectionId, gppModel != null ? gppModel.encode() : null), + samplingRate); + metrics.updateAlertsMetrics(MetricName.general); + + return 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/creator/rule/PrivacyModulesRuleCreator.java b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java index bb68ff60df0..80c13e322d9 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/creator/rule/PrivacyModulesRuleCreator.java @@ -7,8 +7,13 @@ import org.prebid.server.activity.infrastructure.creator.privacy.PrivacyModuleCreator; import org.prebid.server.activity.infrastructure.privacy.PrivacyModule; import org.prebid.server.activity.infrastructure.privacy.PrivacyModuleQualifier; +import org.prebid.server.activity.infrastructure.privacy.SkippedPrivacyModule; import org.prebid.server.activity.infrastructure.rule.AndRule; import org.prebid.server.activity.infrastructure.rule.Rule; +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.model.activity.privacy.AccountPrivacyModuleConfig; import org.prebid.server.settings.model.activity.rule.AccountActivityPrivacyModulesRuleConfig; @@ -22,15 +27,19 @@ public class PrivacyModulesRuleCreator extends AbstractRuleCreator { + private static final Logger logger = LoggerFactory.getLogger(PrivacyModulesRuleCreator.class); + private static final String WILDCARD = "*"; private final Map privacyModulesCreators; + private final Metrics metrics; - public PrivacyModulesRuleCreator(List privacyModulesCreators) { + public PrivacyModulesRuleCreator(List privacyModulesCreators, Metrics metrics) { super(AccountActivityPrivacyModulesRuleConfig.class); this.privacyModulesCreators = Objects.requireNonNull(privacyModulesCreators).stream() .collect(Collectors.toMap(PrivacyModuleCreator::qualifier, UnaryOperator.identity())); + this.metrics = Objects.requireNonNull(metrics); } @Override @@ -43,8 +52,8 @@ protected Rule fromConfiguration(AccountActivityPrivacyModulesRuleConfig ruleCon .map(configuredModuleName -> mapToModulesQualifiers(configuredModuleName, creationContext)) .flatMap(Collection::stream) .filter(qualifier -> !creationContext.isUsed(qualifier)) - .peek(creationContext::use) .map(qualifier -> createPrivacyModule(qualifier, creationContext)) + .filter(Objects::nonNull) .toList(); return new AndRule(privacyModules); @@ -82,8 +91,23 @@ private static boolean isModuleEnabled(AccountPrivacyModuleConfig accountPrivacy private PrivacyModule createPrivacyModule(PrivacyModuleQualifier privacyModuleQualifier, ActivityControllerCreationContext creationContext) { - return privacyModulesCreators.get(privacyModuleQualifier) - .from(creationContext(privacyModuleQualifier, creationContext)); + if (creationContext.getSkipPrivacyModules().contains(privacyModuleQualifier)) { + creationContext.use(privacyModuleQualifier); + return new SkippedPrivacyModule(privacyModuleQualifier); + } + + try { + final PrivacyModule privacyModule = privacyModulesCreators.get(privacyModuleQualifier) + .from(creationContext(privacyModuleQualifier, creationContext)); + creationContext.use(privacyModuleQualifier); + + return privacyModule; + } catch (Exception e) { + logger.error("PrivacyModule %s creation failed: %s.".formatted(privacyModuleQualifier, e.getMessage())); + metrics.updateAlertsMetrics(MetricName.general); + + return null; + } } private static PrivacyModuleCreationContext creationContext(PrivacyModuleQualifier privacyModuleQualifier, 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..f19b839aeec 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 @@ -3,6 +3,9 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.privacy.PrivacyModuleQualifier; +import org.prebid.server.activity.infrastructure.privacy.SkippedPrivacyModule; +import org.prebid.server.activity.infrastructure.rule.AndRule; import org.prebid.server.activity.infrastructure.rule.Rule; import org.prebid.server.json.JacksonMapper; import org.prebid.server.metric.Metrics; @@ -15,14 +18,17 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Objects; +import java.util.Set; public class ActivityInfrastructureDebug { private final String accountId; private final TraceLevel traceLevel; private final List traceLog; + private final Set skippedPrivacyModules; private final Metrics metrics; private final JacksonMapper jacksonMapper; @@ -34,6 +40,7 @@ public ActivityInfrastructureDebug(String accountId, this.accountId = accountId; this.traceLevel = traceLevel; this.traceLog = new ArrayList<>(); + this.skippedPrivacyModules = EnumSet.noneOf(PrivacyModuleQualifier.class); this.metrics = Objects.requireNonNull(metrics); this.jacksonMapper = Objects.requireNonNull(jacksonMapper); } @@ -56,6 +63,8 @@ public void emitActivityInvocationDefaultResult(boolean defaultResult) { } public void emitProcessedRule(Rule rule, Rule.Result result) { + collectSkippedPrivacyModules(rule); + if (atLeast(TraceLevel.basic)) { traceLog.add(ExtTraceActivityRule.of( "Processing rule.", @@ -69,6 +78,17 @@ public void emitProcessedRule(Rule rule, Rule.Result result) { } } + private void collectSkippedPrivacyModules(Rule rule) { + if (rule instanceof SkippedPrivacyModule module) { + skippedPrivacyModules.add(module.skippedModule()); + } else if (rule instanceof AndRule andRule) { + andRule.rules().stream() + .filter(SkippedPrivacyModule.class::isInstance) + .map(SkippedPrivacyModule.class::cast) + .forEach(module -> skippedPrivacyModules.add(module.skippedModule())); + } + } + public void emitActivityInvocationResult(Activity activity, ActivityInvocationPayload activityInvocationPayload, boolean result) { @@ -81,13 +101,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); } } @@ -95,6 +122,10 @@ public List trace() { return Collections.unmodifiableList(traceLog); } + public Set skippedPrivacyModules() { + return Collections.unmodifiableSet(skippedPrivacyModules); + } + private boolean atLeast(TraceLevel minTraceLevel) { return traceLevel != null && traceLevel.ordinal() >= minTraceLevel.ordinal(); } 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..98dbbc91040 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.UsCa; +import com.iab.gpp.encoder.section.UsCo; +import com.iab.gpp.encoder.section.UsCt; +import com.iab.gpp.encoder.section.UsNat; +import com.iab.gpp.encoder.section.UsUt; +import com.iab.gpp.encoder.section.UsVa; 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(UsNat.ID), + CALIFORNIA(UsCa.ID), + VIRGINIA(UsVa.ID), + COLORADO(UsCo.ID), + UTAH(UsUt.ID), + CONNECTICUT(UsCt.ID); public static final Set US_PRIVACY_SECTIONS = Set.of( NATIONAL, diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/SkippedPrivacyModule.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/SkippedPrivacyModule.java new file mode 100644 index 00000000000..fc73a5f67f7 --- /dev/null +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/SkippedPrivacyModule.java @@ -0,0 +1,34 @@ +package org.prebid.server.activity.infrastructure.privacy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.prebid.server.activity.infrastructure.debug.Loggable; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; + +import java.util.Objects; + +public class SkippedPrivacyModule implements PrivacyModule, Loggable { + + private final PrivacyModuleQualifier privacyModuleQualifier; + + public SkippedPrivacyModule(PrivacyModuleQualifier privacyModuleQualifier) { + this.privacyModuleQualifier = Objects.requireNonNull(privacyModuleQualifier); + } + + @Override + public Result proceed(ActivityInvocationPayload activityInvocationPayload) { + return Result.ABSTAIN; + } + + @Override + public JsonNode asLogEntry(ObjectMapper mapper) { + return mapper.createObjectNode() + .put("privacy_module", privacyModuleQualifier.moduleName()) + .put("skipped", true) + .put("result", Result.ABSTAIN.name()); + } + + public PrivacyModuleQualifier skippedModule() { + return privacyModuleQualifier; + } +} 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..4c9ea014fcf 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.UsCa; 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 UsCa consent; public USCaliforniaGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCaV1Section() : null; + consent = gppModel != null ? gppModel.getUsCaSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCa::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCa::getGpc); } @Override @@ -32,17 +32,17 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCa::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCa::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSaleOptOutNotice); } @Override @@ -52,12 +52,12 @@ public Integer getSharingNotice() { @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCa::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSharingOptOutNotice); } @Override @@ -72,12 +72,12 @@ public Integer getTargetedAdvertisingOptOutNotice() { @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSensitiveDataLimitUseNotice); } @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCa::getSensitiveDataProcessing); } @Override @@ -87,26 +87,26 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCa::getKnownChildSensitiveDataConsents); } @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCa::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCa::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCa::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCa::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..ee45b9b59b4 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.UsCo; 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 UsCo consent; public USColoradoGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCoV1Section() : null; + consent = gppModel != null ? gppModel.getUsCoSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCo::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCo::getGpc); } @Override @@ -33,22 +33,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCo::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCo::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getSharingNotice); } @Override @@ -63,12 +63,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCo::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getTargetedAdvertisingOptOutNotice); } @Override @@ -78,7 +78,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCo::getSensitiveDataProcessing); } @Override @@ -88,7 +88,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCo::getKnownChildSensitiveDataConsents); } @Override @@ -98,16 +98,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCo::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCo::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCo::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..5969f65669d 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.UsCt; 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 UsCt consent; public USConnecticutGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCtV1Section() : null; + consent = gppModel != null ? gppModel.getUsCtSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCt::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCt::getGpc); } @Override @@ -32,22 +32,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCt::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCt::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCt::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,7 +77,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCt::getSensitiveDataProcessing); } @Override @@ -87,7 +87,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCt::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCt::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCt::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCt::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..1f32870294b 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.UsUt; 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 UsUt consent; public USUtahGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspUtV1Section() : null; + consent = gppModel != null ? gppModel.getUsUtSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsUt::getVersion); } @Override @@ -37,17 +37,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsUt::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsUt::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,17 +77,17 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsUt::getSensitiveDataProcessing); } @Override public Integer getSensitiveDataProcessingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSensitiveDataProcessingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getSensitiveDataProcessingOptOutNotice); } @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsUt::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsUt::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsUt::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsUt::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..439afbc0865 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.UsVa; 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 UsVa consent; public USVirginiaGppReader(GppModel gppModel) { - this.consent = gppModel != null ? gppModel.getUspVaV1Section() : null; + this.consent = gppModel != null ? gppModel.getUsVaSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsVa::getVersion); } @Override @@ -37,17 +37,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsVa::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getSharingNotice); } @Override @@ -62,12 +62,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsVa::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getTargetedAdvertisingOptOutNotice); } @Override @@ -77,7 +77,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsVa::getSensitiveDataProcessing); } @Override @@ -87,7 +87,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public Integer getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsVa::getKnownChildSensitiveDataConsents); } @Override @@ -97,16 +97,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsVa::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsVa::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsVa::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/USNatModule.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/USNatModule.java index 9208a4d1f6b..e4d3ba048b4 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/USNatModule.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/USNatModule.java @@ -11,6 +11,7 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.inner.USNatSyncUser; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.USNatTransmitGeo; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.USNatTransmitUfpd; +import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.Objects; @@ -18,18 +19,21 @@ public class USNatModule implements PrivacyModule, Loggable { private final PrivacyModule innerModule; - public USNatModule(Activity activity, USNatGppReader gppReader) { + public USNatModule(Activity activity, USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { Objects.requireNonNull(activity); Objects.requireNonNull(gppReader); - innerModule = innerModule(activity, gppReader); + innerModule = innerModule(activity, gppReader, config); } - private static PrivacyModule innerModule(Activity activity, USNatGppReader gppReader) { + private static PrivacyModule innerModule(Activity activity, + USNatGppReader gppReader, + AccountUSNatModuleConfig.Config config) { + return switch (activity) { - case SYNC_USER, MODIFY_UFDP -> new USNatSyncUser(gppReader); - case TRANSMIT_UFPD, TRANSMIT_EIDS -> new USNatTransmitUfpd(gppReader); - case TRANSMIT_GEO -> new USNatTransmitGeo(gppReader); + case SYNC_USER, MODIFY_UFDP -> new USNatSyncUser(gppReader, config); + case TRANSMIT_UFPD, TRANSMIT_EIDS -> new USNatTransmitUfpd(gppReader, config); + case TRANSMIT_GEO -> new USNatTransmitGeo(gppReader, config); case CALL_BIDDER, TRANSMIT_TID, REPORT_ANALYTICS -> USNatDefault.instance(); }; } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatSyncUser.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatSyncUser.java index 77a6bfea09b..d71522b02e1 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatSyncUser.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatSyncUser.java @@ -19,6 +19,7 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.TargetedAdvertisingOptOut; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.TargetedAdvertisingOptOutNotice; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.USNatField; +import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; import java.util.Objects; @@ -28,10 +29,10 @@ public class USNatSyncUser implements PrivacyModule, Loggable { private final USNatGppReader gppReader; private final Result result; - public USNatSyncUser(USNatGppReader gppReader) { + public USNatSyncUser(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { this.gppReader = gppReader; - result = disallow(gppReader) ? Result.DISALLOW : Result.ALLOW; + result = disallow(gppReader, config) ? Result.DISALLOW : Result.ALLOW; } @Override @@ -39,14 +40,14 @@ public Result proceed(ActivityInvocationPayload activityInvocationPayload) { return result; } - public static boolean disallow(USNatGppReader gppReader) { + public static boolean disallow(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { return equals(gppReader.getMspaServiceProviderMode(), MspaServiceProviderMode.YES) || equals(gppReader.getGpc(), Gpc.TRUE) || checkSale(gppReader) || checkSharing(gppReader) || checkTargetedAdvertising(gppReader) || checkKnownChildSensitiveDataConsents(gppReader) - || equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + || checkPersonalDataConsents(gppReader, config); } private static boolean checkSale(USNatGppReader gppReader) { @@ -88,9 +89,15 @@ private static boolean checkKnownChildSensitiveDataConsents(USNatGppReader gppRe return equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 0) || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 1) + || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 2) || equalsAtIndex(KnownChildSensitiveDataConsent.CONSENT, knownChildSensitiveDataConsents, 1); } + private static boolean checkPersonalDataConsents(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { + return (config == null || !config.isAllowPersonalDataConsent2()) + && equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + } + private static boolean equalsAtIndex(USNatField expectedValue, List list, int index) { return list != null && list.size() > index && equals(list.get(index), expectedValue); } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitGeo.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitGeo.java index dba489b2f0c..f4daba2c51c 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitGeo.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitGeo.java @@ -15,6 +15,7 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.SensitiveDataProcessing; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.SensitiveDataProcessingOptOutNotice; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.USNatField; +import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; import java.util.Objects; @@ -24,10 +25,10 @@ public class USNatTransmitGeo implements PrivacyModule, Loggable { private final USNatGppReader gppReader; private final Result result; - public USNatTransmitGeo(USNatGppReader gppReader) { + public USNatTransmitGeo(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { this.gppReader = gppReader; - result = disallow(gppReader) ? Result.DISALLOW : Result.ALLOW; + result = disallow(gppReader, config) ? Result.DISALLOW : Result.ALLOW; } @Override @@ -35,12 +36,12 @@ public Result proceed(ActivityInvocationPayload activityInvocationPayload) { return result; } - public static boolean disallow(USNatGppReader gppReader) { + public static boolean disallow(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { return equals(gppReader.getMspaServiceProviderMode(), MspaServiceProviderMode.YES) || equals(gppReader.getGpc(), Gpc.TRUE) || checkSensitiveData(gppReader) || checkKnownChildSensitiveDataConsents(gppReader) - || equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + || checkPersonalDataConsents(gppReader, config); } private static boolean checkSensitiveData(USNatGppReader gppReader) { @@ -61,9 +62,15 @@ private static boolean checkKnownChildSensitiveDataConsents(USNatGppReader gppRe return equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 0) || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 1) + || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 2) || equalsAtIndex(KnownChildSensitiveDataConsent.CONSENT, knownChildSensitiveDataConsents, 1); } + private static boolean checkPersonalDataConsents(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { + return (config == null || !config.isAllowPersonalDataConsent2()) + && equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + } + private static boolean equalsAtIndex(USNatField expectedValue, List list, int index) { return list != null && list.size() > index && equals(list.get(index), expectedValue); } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitUfpd.java b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitUfpd.java index 248c9705e09..f3fe60a4923 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitUfpd.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/privacy/usnat/inner/USNatTransmitUfpd.java @@ -22,6 +22,7 @@ import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.TargetedAdvertisingOptOut; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.TargetedAdvertisingOptOutNotice; import org.prebid.server.activity.infrastructure.privacy.usnat.inner.model.USNatField; +import org.prebid.server.settings.model.activity.privacy.AccountUSNatModuleConfig; import java.util.List; import java.util.Objects; @@ -29,17 +30,18 @@ public class USNatTransmitUfpd implements PrivacyModule, Loggable { - private static final Set SENSITIVE_DATA_INDICES_SET_1 = Set.of(0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11); - private static final Set SENSITIVE_DATA_INDICES_SET_2 = Set.of(0, 1, 2, 3, 4, 10); - private static final Set SENSITIVE_DATA_INDICES_SET_3 = Set.of(5, 6, 8, 9, 11); + private static final Set SENSITIVE_DATA_INDICES_SET_1 = Set.of( + 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15); + private static final Set SENSITIVE_DATA_INDICES_SET_2 = Set.of(0, 1, 2, 3, 4, 10, 12, 14); + private static final Set SENSITIVE_DATA_INDICES_SET_3 = Set.of(5, 6, 8, 9, 11, 13, 15); private final USNatGppReader gppReader; private final Result result; - public USNatTransmitUfpd(USNatGppReader gppReader) { + public USNatTransmitUfpd(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { this.gppReader = gppReader; - result = disallow(gppReader) ? Result.DISALLOW : Result.ALLOW; + result = disallow(gppReader, config) ? Result.DISALLOW : Result.ALLOW; } @Override @@ -47,7 +49,7 @@ public Result proceed(ActivityInvocationPayload activityInvocationPayload) { return result; } - public static boolean disallow(USNatGppReader gppReader) { + public static boolean disallow(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { return equals(gppReader.getMspaServiceProviderMode(), MspaServiceProviderMode.YES) || equals(gppReader.getGpc(), Gpc.TRUE) || checkSale(gppReader) @@ -55,7 +57,7 @@ public static boolean disallow(USNatGppReader gppReader) { || checkTargetedAdvertising(gppReader) || checkSensitiveData(gppReader) || checkKnownChildSensitiveDataConsents(gppReader) - || equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + || checkPersonalDataConsents(gppReader, config); } private static boolean checkSale(USNatGppReader gppReader) { @@ -124,9 +126,15 @@ private static boolean checkKnownChildSensitiveDataConsents(USNatGppReader gppRe return equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 0) || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 1) + || equalsAtIndex(KnownChildSensitiveDataConsent.NO_CONSENT, knownChildSensitiveDataConsents, 2) || equalsAtIndex(KnownChildSensitiveDataConsent.CONSENT, knownChildSensitiveDataConsents, 1); } + private static boolean checkPersonalDataConsents(USNatGppReader gppReader, AccountUSNatModuleConfig.Config config) { + return (config == null || !config.isAllowPersonalDataConsent2()) + && equals(gppReader.getPersonalDataConsents(), PersonalDataConsents.CONSENT); + } + private static boolean anyEqualsAtIndices(USNatField expectedValue, List list, Set indices) { return indices.stream().anyMatch(index -> equalsAtIndex(expectedValue, list, index)); } 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..903275b9d43 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.UsCa; 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 UsCa consent; public USMappedCaliforniaGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCaV1Section() : null; + consent = gppModel != null ? gppModel.getUsCaSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCa::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCa::getGpc); } @Override @@ -38,17 +38,17 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCa::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCa::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSaleOptOutNotice); } @Override @@ -58,12 +58,12 @@ public Integer getSharingNotice() { @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCa::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSharingOptOutNotice); } @Override @@ -78,7 +78,7 @@ public Integer getTargetedAdvertisingOptOutNotice() { @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsCa::getSensitiveDataLimitUseNotice); } @Override @@ -117,21 +117,21 @@ public List getKnownChildSensitiveDataConsents() { @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsCa::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCa::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCa::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCa::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..fafc908c7bb 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.UsCo; 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 UsCo consent; public USMappedColoradoGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCoV1Section() : null; + consent = gppModel != null ? gppModel.getUsCoSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCo::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCo::getGpc); } @Override @@ -36,22 +36,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCo::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCo::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getSharingNotice); } @Override @@ -66,12 +66,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCo::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCo::getTargetedAdvertisingOptOutNotice); } @Override @@ -81,7 +81,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCo::getSensitiveDataProcessing); } @Override @@ -108,16 +108,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsCo::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCo::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCoV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCo::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..c7042bbbedf 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.UsCt; 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 UsCt consent; public USMappedConnecticutGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspCtV1Section() : null; + consent = gppModel != null ? gppModel.getUsCtSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsCt::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsCt::getGpc); } @Override @@ -38,22 +38,22 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsCt::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsCt::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getSharingNotice); } @Override @@ -68,12 +68,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsCt::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsCt::getTargetedAdvertisingOptOutNotice); } @Override @@ -83,7 +83,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsCt::getSensitiveDataProcessing); } @Override @@ -94,7 +94,7 @@ public Integer getSensitiveDataProcessingOptOutNotice() { @Override public List getKnownChildSensitiveDataConsents() { final List originalData = ObjectUtil.getIfNotNull( - consent, UspCtV1::getKnownChildSensitiveDataConsents); + consent, UsCt::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, UsCt::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsCt::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspCtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsCt::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..53eb837e82f 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.UsUt; 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 UsUt consent; public USMappedUtahGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspUtV1Section() : null; + consent = gppModel != null ? gppModel.getUsUtSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsUt::getVersion); } @Override @@ -45,17 +45,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsUt::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getSharingNotice); } @Override @@ -70,12 +70,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsUt::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsUt::getTargetedAdvertisingOptOutNotice); } @Override @@ -86,7 +86,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { final List originalData = Optional.ofNullable(consent) - .map(UspUtV1::getSensitiveDataProcessing) + .map(UsUt::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, UsUt::getSensitiveDataProcessingOptOutNotice); } @Override @@ -126,16 +126,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsUt::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsUt::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspUtV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsUt::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..146730d83a4 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.UsVa; 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 UsVa consent; public USMappedVirginiaGppReader(GppModel gppModel) { - this.consent = gppModel != null ? gppModel.getUspVaV1Section() : null; + this.consent = gppModel != null ? gppModel.getUsVaSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsVa::getVersion); } @Override @@ -41,17 +41,17 @@ public Boolean getGpcSegmentIncluded() { @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsVa::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getSharingNotice); } @Override @@ -66,12 +66,12 @@ public Integer getSharingOptOutNotice() { @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsVa::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsVa::getTargetedAdvertisingOptOutNotice); } @Override @@ -81,7 +81,7 @@ public Integer getSensitiveDataLimitUseNotice() { @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsVa::getSensitiveDataProcessing); } @Override @@ -108,16 +108,16 @@ public Integer getPersonalDataConsents() { @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsVa::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsVa::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspVaV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsVa::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..9cf427b872b 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.UsNat; 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 UsNat consent; public USNationalGppReader(GppModel gppModel) { - consent = gppModel != null ? gppModel.getUspNatV1Section() : null; + consent = gppModel != null ? gppModel.getUsNatSection() : null; } @Override public Integer getVersion() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getVersion); + return ObjectUtil.getIfNotNull(consent, UsNat::getVersion); } @Override public Boolean getGpc() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getGpc); + return ObjectUtil.getIfNotNull(consent, UsNat::getGpc); } @Override @@ -37,81 +37,81 @@ public Boolean getGpcSegmentType() { @Override public Boolean getGpcSegmentIncluded() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getGpcSegmentIncluded); + return ObjectUtil.getIfNotNull(consent, UsNat::getGpcSegmentIncluded); } @Override public Integer getSaleOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSaleOptOut); + return ObjectUtil.getIfNotNull(consent, UsNat::getSaleOptOut); } @Override public Integer getSaleOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSaleOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getSaleOptOutNotice); } @Override public Integer getSharingNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getSharingNotice); } @Override public Integer getSharingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingOptOut); + return ObjectUtil.getIfNotNull(consent, UsNat::getSharingOptOut); } @Override public Integer getSharingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSharingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getSharingOptOutNotice); } @Override public Integer getTargetedAdvertisingOptOut() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getTargetedAdvertisingOptOut); + return ObjectUtil.getIfNotNull(consent, UsNat::getTargetedAdvertisingOptOut); } @Override public Integer getTargetedAdvertisingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getTargetedAdvertisingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getTargetedAdvertisingOptOutNotice); } @Override public Integer getSensitiveDataLimitUseNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataLimitUseNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getSensitiveDataLimitUseNotice); } @Override public List getSensitiveDataProcessing() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataProcessing); + return ObjectUtil.getIfNotNull(consent, UsNat::getSensitiveDataProcessing); } @Override public Integer getSensitiveDataProcessingOptOutNotice() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getSensitiveDataProcessingOptOutNotice); + return ObjectUtil.getIfNotNull(consent, UsNat::getSensitiveDataProcessingOptOutNotice); } @Override public List getKnownChildSensitiveDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getKnownChildSensitiveDataConsents); + return ObjectUtil.getIfNotNull(consent, UsNat::getKnownChildSensitiveDataConsents); } @Override public Integer getPersonalDataConsents() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getPersonalDataConsents); + return ObjectUtil.getIfNotNull(consent, UsNat::getPersonalDataConsents); } @Override public Integer getMspaCoveredTransaction() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaCoveredTransaction); + return ObjectUtil.getIfNotNull(consent, UsNat::getMspaCoveredTransaction); } @Override public Integer getMspaServiceProviderMode() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaServiceProviderMode); + return ObjectUtil.getIfNotNull(consent, UsNat::getMspaServiceProviderMode); } @Override public Integer getMspaOptOutOptionMode() { - return ObjectUtil.getIfNotNull(consent, UspNatV1::getMspaOptOutOptionMode); + return ObjectUtil.getIfNotNull(consent, UsNat::getMspaOptOutOptionMode); } } diff --git a/src/main/java/org/prebid/server/activity/infrastructure/rule/AndRule.java b/src/main/java/org/prebid/server/activity/infrastructure/rule/AndRule.java index 40585181541..31163933d68 100644 --- a/src/main/java/org/prebid/server/activity/infrastructure/rule/AndRule.java +++ b/src/main/java/org/prebid/server/activity/infrastructure/rule/AndRule.java @@ -8,6 +8,7 @@ import org.prebid.server.activity.infrastructure.debug.Loggable; import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -45,4 +46,8 @@ public JsonNode asLogEntry(ObjectMapper mapper) { return andNode; } + + public List rules() { + return Collections.unmodifiableList(rules); + } } 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/model/VideoEvent.java b/src/main/java/org/prebid/server/analytics/model/VideoEvent.java index 0f2519f95df..f998d826678 100644 --- a/src/main/java/org/prebid/server/analytics/model/VideoEvent.java +++ b/src/main/java/org/prebid/server/analytics/model/VideoEvent.java @@ -25,4 +25,3 @@ public class VideoEvent { VideoResponse bidResponse; } - 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..28e14a22deb 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,17 +47,15 @@ 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); - private static final ConditionalLogger UNKNOWN_ADAPTERS_LOGGER = new ConditionalLogger(logger); + private static final ConditionalLogger unknownAdaptersLogger = new ConditionalLogger(logger); private static final Set ADAPTERS_PERMITTED_FOR_FULL_DATA = Collections.singleton("logAnalytics"); private final Vertx vertx; @@ -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); } } @@ -153,7 +162,7 @@ private void logUnknownAdapters(AuctionEvent auctionEvent) { if (CollectionUtils.isNotEmpty(unknownAdapterNames)) { final Site site = bidRequest.getSite(); final String refererUrl = site != null ? site.getPage() : null; - UNKNOWN_ADAPTERS_LOGGER.warn("Unknown adapters in ext.prebid.analytics[].adapter: %s, referrer: '%s'" + unknownAdaptersLogger.warn("Unknown adapters in ext.prebid.analytics[].adapter: %s, referrer: '%s'" .formatted(unknownAdapterNames, refererUrl), logSamplingRate); } } @@ -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..ed99c241ee5 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java @@ -0,0 +1,273 @@ +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 String eventType = contextAndType.getRight(); + if (auctionContext == null) { + return Future.succeededFuture(); + } + + 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(eventType) + .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..e7b6dca1eae --- /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; + + 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/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..c37ac7aeff2 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java @@ -0,0 +1,508 @@ +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.apache.commons.lang3.StringUtils; +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.ExplorationResult; +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.GreenbidsConfig; +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.analytics.reporter.greenbids.model.Ortb2ImpExtResult; +import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +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.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +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.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.StreamUtil; +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 GreenbidsConfig greenbidsConfig = Optional.ofNullable(parseBidRequestExt(auctionContext)) + .orElseGet(() -> parseAccountConfig(auctionContext.getAccount())); + + if (greenbidsConfig == null) { + return Future.succeededFuture(); + } + + final String billingId = UUID.randomUUID().toString(); + + final Map analyticsResultFromAnalyticsTag = extractAnalyticsResultFromAnalyticsTag( + auctionContext); + + final String greenbidsId = greenbidsId(analyticsResultFromAnalyticsTag); + + final double samplingRate = resolveSamplingRate(greenbidsConfig); + + if (!isSampled(samplingRate, greenbidsId)) { + return Future.succeededFuture(); + } + + final String commonMessageJson; + try { + final CommonMessage commonMessage = createBidMessage( + auctionContext, + bidResponse, + greenbidsId, + billingId, + greenbidsConfig, + analyticsResultFromAnalyticsTag, + samplingRate); + 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 GreenbidsConfig parseBidRequestExt(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getBidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .filter(this::isNotEmptyObjectNode) + .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME)) + .map(this::toGreenbidsConfig) + .orElse(null); + } + + private boolean isNotEmptyObjectNode(JsonNode analytics) { + return analytics != null && analytics.isObject() && !analytics.isEmpty(); + } + + private GreenbidsConfig parseAccountConfig(Account account) { + return Optional.ofNullable(account) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::getModules) + .map(analyticsModules -> analyticsModules.get(name())) + .map(this::toGreenbidsConfig) + .orElse(null); + } + + private GreenbidsConfig toGreenbidsConfig(ObjectNode adapterNode) { + try { + return jacksonMapper.mapper().treeToValue(adapterNode, GreenbidsConfig.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding bid request analytics extension: " + e.getMessage(), e); + } + } + + private Map extractAnalyticsResultFromAnalyticsTag(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> "greenbids-real-time-data".equals(hook.getHookId().getModuleCode())) + .filter(hook -> hook.getStatus() == ExecutionStatus.success) + .map(HookExecutionOutcome::getAnalyticsTags) + .map(Tags::activities) + .flatMap(Collection::stream) + .filter(activity -> "greenbids-filter".equals(activity.name())) + .map(Activity::results) + .flatMap(Collection::stream) + .map(this::parseAnalyticsResult) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (existing, replacement) -> existing)); + } + + private Map parseAnalyticsResult(Result result) { + return Optional.ofNullable(result) + .map(Result::values) + .stream() + .flatMap(valuesNode -> StreamUtil.asStream(valuesNode.fields())) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> parseOrtb2ImpExtResult(entry.getValue()), + (existing, replacement) -> existing)); + } + + private Ortb2ImpExtResult parseOrtb2ImpExtResult(JsonNode node) { + try { + return jacksonMapper.mapper().treeToValue(node, Ortb2ImpExtResult.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Analytics result parsing error", e); + } + } + + private String greenbidsId(Map analyticsResultFromAnalyticsTag) { + return Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(Map::values) + .map(Collection::stream) + .flatMap(Stream::findFirst) + .map(Ortb2ImpExtResult::getGreenbids) + .map(ExplorationResult::getFingerprint) + .orElseGet(() -> UUID.randomUUID().toString()); + } + + private double resolveSamplingRate(GreenbidsConfig greenbidsConfig) { + final Double sampling = greenbidsConfig.getGreenbidsSampling(); + if (sampling == null) { + logger.warn("Warning: Sampling rate is not defined in request. Set sampling at {}", + greenbidsAnalyticsProperties.getDefaultSamplingRate()); + return greenbidsAnalyticsProperties.getDefaultSamplingRate(); + } + return sampling; + } + + 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 < 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, + GreenbidsConfig greenbidsConfig, + Map analyticsResultFromAnalyticsTag, + Double samplingRate) { + 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(), analyticsResultFromAnalyticsTag)) + .toList(); + + final String auctionId = bidRequest + .map(BidRequest::getId) + .orElse(null); + + final String referrer = bidRequest + .map(BidRequest::getSite) + .map(Site::getPage) + .orElse(null); + + final String pbuid = Optional.ofNullable(greenbidsConfig.getPbuid()).orElse(StringUtils.EMPTY); + + return CommonMessage.builder() + .version(greenbidsAnalyticsProperties.getAnalyticsServerVersion()) + .auctionId(auctionId) + .referrer(referrer) + .sampling(samplingRate) + .prebidServer(prebidVersionProvider.getNameVersionRecord()) + .greenbidsId(greenbidsId) + .pbuid(pbuid) + .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.getRejected().stream() + .map(rejectedImp -> NonBid.of(rejectedImp.impId(), rejectedImp.reason())) + .toList(); + + return SeatNonBid.of(bidder, nonBids); + } + + private GreenbidsAdUnit createAdUnit( + Imp imp, + Map seatsWithBids, + Map seatsWithNonBids, + String currency, + Map analyticsResultFromAnalyticsTag) { + 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); + + final Ortb2ImpResult ortb2ImpResult = Optional.ofNullable(analyticsResultFromAnalyticsTag) + .map(analyticsResult -> analyticsResult.get(imp.getId())) + .map(Ortb2ImpResult::of) + .orElse(null); + + return GreenbidsAdUnit.builder() + .code(adUnitCode) + .unifiedCode(greenbidsUnifiedCode) + .mediaTypes(mediaTypes) + .bids(bids) + .ortb2ImpResult(ortb2ImpResult) + .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/ExplorationResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java new file mode 100644 index 00000000000..48a2a0e8038 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java @@ -0,0 +1,18 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExplorationResult { + + String fingerprint; + + @JsonProperty("keptInAuction") + Map keptInAuction; + + @JsonProperty("isExploration") + Boolean isExploration; +} 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..52f0ebab684 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java @@ -0,0 +1,25 @@ +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; + + @JsonProperty("ortb2Imp") + Ortb2ImpResult ortb2ImpResult; +} 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/GreenbidsConfig.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsConfig.java new file mode 100644 index 00000000000..2820daea092 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsConfig.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 GreenbidsConfig { + + String pbuid; + + @JsonProperty("greenbids-sampling") + 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/greenbids/model/Ortb2ImpExtResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java new file mode 100644 index 00000000000..c6cc8350bd8 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java @@ -0,0 +1,11 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpExtResult { + + ExplorationResult greenbids; + + String tid; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java new file mode 100644 index 00000000000..377bd3c677a --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java @@ -0,0 +1,9 @@ +package org.prebid.server.analytics.reporter.greenbids.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Ortb2ImpResult { + + Ortb2ImpExtResult ext; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/liveintent/LiveIntentAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/liveintent/LiveIntentAnalyticsReporter.java new file mode 100644 index 00000000000..d404a12397e --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/liveintent/LiveIntentAnalyticsReporter.java @@ -0,0 +1,195 @@ +package org.prebid.server.analytics.reporter.liveintent; + +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 io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.http.client.utils.URIBuilder; +import org.prebid.server.analytics.AnalyticsReporter; +import org.prebid.server.analytics.model.AuctionEvent; +import org.prebid.server.analytics.model.NotificationEvent; +import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties; +import org.prebid.server.analytics.reporter.liveintent.model.PbsjBid; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +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.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; +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.vertx.httpclient.HttpClient; + +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class LiveIntentAnalyticsReporter implements AnalyticsReporter { + + private static final Logger logger = LoggerFactory.getLogger(LiveIntentAnalyticsReporter.class); + + private static final String LIVEINTENT_HOOK_ID = "liveintent-omni-channel-identity-enrichment-hook"; + + private final HttpClient httpClient; + private final LiveIntentAnalyticsProperties properties; + private final JacksonMapper jacksonMapper; + + public LiveIntentAnalyticsReporter( + LiveIntentAnalyticsProperties properties, + HttpClient httpClient, + JacksonMapper jacksonMapper) { + + this.httpClient = Objects.requireNonNull(httpClient); + this.properties = Objects.requireNonNull(properties); + this.jacksonMapper = Objects.requireNonNull(jacksonMapper); + } + + @Override + public Future processEvent(T event) { + if (event instanceof AuctionEvent auctionEvent) { + return processAuctionEvent(auctionEvent.getAuctionContext()); + } else if (event instanceof NotificationEvent notificationEvent) { + return processNotificationEvent(notificationEvent); + } + + return Future.succeededFuture(); + } + + private Future processAuctionEvent(AuctionContext auctionContext) { + if (auctionContext.getBidRequest() == null) { + return Future.failedFuture(new PreBidException("Bid request should not be empty")); + } + + if (auctionContext.getBidResponse() == null) { + return Future.succeededFuture(); + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidResponse bidResponse = auctionContext.getBidResponse(); + + final List activities = getActivities(auctionContext); + final boolean isEnriched = isEnriched(activities); + final Float treatmentRate = getTreatmentRate(activities); + final Long timestamp = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAuctiontimestamp) + .orElse(0L); + + final List pbsjBids = CollectionUtils.emptyIfNull(bidResponse.getSeatbid()).stream() + .map(SeatBid::getBid) + .flatMap(Collection::stream) + .map(bid -> buildPbsjBid(bidRequest, bidResponse, bid, isEnriched, treatmentRate, timestamp)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + try { + return httpClient.post( + new URIBuilder(properties.getAnalyticsEndpoint()) + .setPath("/analytic-events/pbsj-bids") + .build() + .toString(), + jacksonMapper.encodeToString(pbsjBids), + properties.getTimeoutMs()) + .mapEmpty(); + } catch (Exception e) { + logger.error("Error processing event: {}", e.getMessage()); + return Future.failedFuture(e); + } + } + + private List getActivities(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> LIVEINTENT_HOOK_ID.equals(hook.getHookId().getModuleCode()) + && hook.getStatus() == ExecutionStatus.success) + .map(HookExecutionOutcome::getAnalyticsTags) + .filter(Objects::nonNull) + .map(Tags::activities) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .toList(); + } + + private boolean isEnriched(List activity) { + return activity.stream().anyMatch(a -> "liveintent-enriched".equals(a.name())); + } + + private Float getTreatmentRate(List activity) { + return activity.stream() + .flatMap(a -> a.results().stream()) + .filter(a -> a.values().has("treatmentRate")) + .findFirst() + .map(a -> a.values().get("treatmentRate").floatValue()) + .orElse(null); + } + + private Optional buildPbsjBid( + BidRequest bidRequest, + BidResponse bidResponse, + Bid bid, + boolean isEnriched, + Float treatmentRate, + Long timestamp) { + + return bidRequest.getImp().stream() + .filter(impItem -> impItem.getId().equals(bid.getImpid())) + .map(imp -> PbsjBid.builder() + .bidId(bid.getId()) + .price(bid.getPrice()) + .adUnitId(imp.getTagid()) + .enriched(isEnriched) + .currency(bidResponse.getCur()) + .treatmentRate(treatmentRate) + .timestamp(timestamp) + .partnerId(properties.getPartnerId()) + .build()) + .findFirst(); + } + + private Future processNotificationEvent(NotificationEvent notificationEvent) { + try { + final String url = new URIBuilder(properties.getAnalyticsEndpoint()) + .setPath("/analytic-events/pbsj-winning-bid") + .setParameter("b", notificationEvent.getBidder()) + .setParameter("bidId", notificationEvent.getBidId()) + .build() + .toString(); + return httpClient.get(url, properties.getTimeoutMs()).mapEmpty(); + } catch (URISyntaxException e) { + logger.error("Error composing url for notification event: {}", e.getMessage()); + return Future.failedFuture(e); + } + } + + @Override + public int vendorId() { + return 0; + } + + @Override + public String name() { + return "liveintentAnalytics"; + } +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/LiveIntentAnalyticsProperties.java b/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/LiveIntentAnalyticsProperties.java new file mode 100644 index 00000000000..fea06b81047 --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/LiveIntentAnalyticsProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.analytics.reporter.liveintent.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class LiveIntentAnalyticsProperties { + + String partnerId; + + String analyticsEndpoint; + + long timeoutMs; +} diff --git a/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/PbsjBid.java b/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/PbsjBid.java new file mode 100644 index 00000000000..00de59f9e3b --- /dev/null +++ b/src/main/java/org/prebid/server/analytics/reporter/liveintent/model/PbsjBid.java @@ -0,0 +1,27 @@ +package org.prebid.server.analytics.reporter.liveintent.model; + +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder(toBuilder = true) +@Value +public class PbsjBid { + + String bidId; + + boolean enriched; + + BigDecimal price; + + String adUnitId; + + String currency; + + Float treatmentRate; + + Long timestamp; + + String partnerId; +} 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/analytics/reporter/pubstack/model/EventType.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java index 24fde84cc3f..3da1eb96961 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/EventType.java @@ -4,4 +4,3 @@ public enum EventType { amp, auction, cookiesync, notification, setuid, video } - diff --git a/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java index 27562b9e40e..e36afbe4236 100644 --- a/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java +++ b/src/main/java/org/prebid/server/analytics/reporter/pubstack/model/PubstackConfig.java @@ -1,13 +1,11 @@ package org.prebid.server.analytics.reporter.pubstack.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Map; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class PubstackConfig { @JsonProperty("scopeId") 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..f1c3d1f0d82 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -18,21 +18,25 @@ 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; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.auction.categorymapping.CategoryMappingService; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidInfo; -import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.BidderResponseInfo; import org.prebid.server.auction.model.CachedDebugLog; import org.prebid.server.auction.model.CategoryMappingResult; import org.prebid.server.auction.model.MultiBidConfig; +import org.prebid.server.auction.model.PaaFormat; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.auction.model.TargetingInfo; import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; @@ -41,27 +45,29 @@ 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; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.identity.IdGenerator; -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.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.ExtImp; import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; @@ -76,18 +82,21 @@ import org.prebid.server.proto.openrtb.ext.response.CacheAsset; import org.prebid.server.proto.openrtb.ext.response.Events; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; 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.ExtIgi; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgb; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgsExt; 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; @@ -95,10 +104,12 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; +import org.prebid.server.settings.model.AccountBidRankingConfig; 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.spring.config.model.CacheDefaultTtlProperties; +import org.prebid.server.util.ListUtil; import org.prebid.server.util.StreamUtil; import org.prebid.server.vast.VastModifier; @@ -107,9 +118,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -117,62 +128,88 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; public class BidResponseCreator { + private static final Logger logger = LoggerFactory.getLogger(BidResponseCreator.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private static final String CACHE = "cache"; private static final String PREBID_EXT = "prebid"; 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 static final int MIN_BID_ID_LENGTH = 17; - private final CacheService cacheService; + private final double logSamplingRate; + private final CoreCacheService coreCacheService; private final BidderCatalog bidderCatalog; private final VastModifier vastModifier; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; private final WinningBidComparatorFactory winningBidComparatorFactory; private final IdGenerator bidIdGenerator; + private final IdGenerator enforcedBidIdGenerator; private final HookStageExecutor hookStageExecutor; private final CategoryMappingService categoryMappingService; private final int truncateAttrChars; + private final boolean enforceRandomBidId; private final Clock clock; private final JacksonMapper mapper; + private final Metrics metrics; + private final CacheTtl mediaTypeCacheTtl; + private final CacheDefaultTtlProperties cacheDefaultProperties; private final String cacheHost; private final String cachePath; private final String cacheAssetUrlTemplate; - public BidResponseCreator(CacheService cacheService, + public BidResponseCreator(double logSamplingRate, + CoreCacheService coreCacheService, BidderCatalog bidderCatalog, VastModifier vastModifier, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, WinningBidComparatorFactory winningBidComparatorFactory, IdGenerator bidIdGenerator, + IdGenerator enforcedBidIdGenerator, HookStageExecutor hookStageExecutor, CategoryMappingService categoryMappingService, int truncateAttrChars, + boolean enforceRandomBidId, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + Metrics metrics, + CacheTtl mediaTypeCacheTtl, + CacheDefaultTtlProperties cacheDefaultProperties) { - 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); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); this.winningBidComparatorFactory = Objects.requireNonNull(winningBidComparatorFactory); this.bidIdGenerator = Objects.requireNonNull(bidIdGenerator); + this.enforcedBidIdGenerator = Objects.requireNonNull(enforcedBidIdGenerator); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.categoryMappingService = Objects.requireNonNull(categoryMappingService); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); + this.enforceRandomBidId = enforceRandomBidId; this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); + this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl); + this.cacheDefaultProperties = Objects.requireNonNull(cacheDefaultProperties); + this.metrics = Objects.requireNonNull(metrics); + this.logSamplingRate = logSamplingRate; - cacheHost = Objects.requireNonNull(cacheService.getEndpointHost()); - cachePath = Objects.requireNonNull(cacheService.getEndpointPath()); - cacheAssetUrlTemplate = Objects.requireNonNull(cacheService.getCachedAssetURLTemplate()); + cacheAssetUrlTemplate = Objects.requireNonNull(coreCacheService.getCachedAssetURLTemplate()); + cacheHost = Objects.requireNonNull(coreCacheService.getEndpointHost()); + cachePath = Objects.requireNonNull(coreCacheService.getEndpointPath()); } private static int validateTruncateAttrChars(int truncateAttrChars) { @@ -182,68 +219,57 @@ 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 create(AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, Map bidderToMultiBids) { - final List auctionParticipations = auctionContext.getAuctionParticipations(); - final List imps = auctionContext.getBidRequest().getImp(); - final EventsContext eventsContext = createEventsContext(auctionContext); + 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 List bidderResponses = auctionParticipations.stream() + final EventsContext eventsContext = createEventsContext(auctionContext); + 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<>(); for (final BidderResponse bidderResponse : bidderResponses) { final String bidder = bidderResponse.getBidder(); - final List modifiedBidderBids = new ArrayList<>(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); for (final BidderBid bidderBid : seatBid.getBids()) { 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 +277,7 @@ private List updateBids(List bidderResponses, result.add(bidderResponse.with(modifiedSeatBid)); } - return result; + return Future.succeededFuture(result); } private Bid updateBid(Bid bid, @@ -259,26 +285,24 @@ 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(); - final String generatedBidId = bidIdGenerator.getType() != IdGeneratorType.none - ? bidIdGenerator.generateId() - : null; - final String effectiveBidId = ObjectUtils.defaultIfNull(generatedBidId, bid.getId()); + final String generatedBidId = bidIdGenerator.generateId(); + final String enforcedRandomBidId = enforcedBidId(bid); + final String effectiveBidId = ObjectUtils.defaultIfNull(generatedBidId, enforcedRandomBidId); return bid.toBuilder() + .id(enforcedRandomBidId) .adm(updateBidAdm(bid, bidType, bidder, account, eventsContext, effectiveBidId, - debugWarnings, - lineItemId)) + debugWarnings)) .ext(updateBidExt( bid, bidType, @@ -287,19 +311,24 @@ private Bid updateBid(Bid bid, videoStoredDataResult, eventsContext, generatedBidId, - effectiveBidId, - lineItemId)) + effectiveBidId)) .build(); } + private String enforcedBidId(Bid bid) { + final int bidIdLength = Optional.ofNullable(bid.getId()).map(String::length).orElse(0); + return enforceRandomBidId && bidIdLength < MIN_BID_ID_LENGTH + ? enforcedBidIdGenerator.generateId() + : bid.getId(); + } + private String updateBidAdm(Bid bid, BidType bidType, String bidder, Account account, EventsContext eventsContext, String effectiveBidId, - List debugWarnings, - String lineItemId) { + List debugWarnings) { final String bidAdm = bid.getAdm(); return BidType.video.equals(bidType) @@ -310,8 +339,7 @@ private String updateBidAdm(Bid bid, effectiveBidId, account.getId(), eventsContext, - debugWarnings, - lineItemId) + debugWarnings) : bidAdm; } @@ -322,8 +350,7 @@ private ObjectNode updateBidExt(Bid bid, VideoStoredDataResult videoStoredDataResult, EventsContext eventsContext, String generatedBidId, - String effectiveBidId, - String lineItemId) { + String effectiveBidId) { final ExtBidPrebid updatedExtBidPrebid = updateBidExtPrebid( bid, @@ -333,10 +360,8 @@ private ObjectNode updateBidExt(Bid bid, videoStoredDataResult, eventsContext, generatedBidId, - effectiveBidId, - lineItemId); + effectiveBidId); final ObjectNode existingBidExt = bid.getExt(); - final ObjectNode updatedBidExt = mapper.mapper().createObjectNode(); if (existingBidExt != null && !existingBidExt.isEmpty()) { @@ -344,7 +369,6 @@ private ObjectNode updateBidExt(Bid bid, } updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(updatedExtBidPrebid)); - return updatedBidExt; } @@ -355,13 +379,11 @@ 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) .map(ExtBidPrebid::toBuilder) .orElseGet(ExtBidPrebid::builder); @@ -385,53 +407,104 @@ 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(); + for (final BidderResponse bidderResponse : bidderResponses) { final String bidder = bidderResponse.getBidder(); - - final List bidInfos = new ArrayList<>(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - 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); - bidInfos.add(bidInfo); + final Map> seatToBids = seatBid.getBids().stream() + .filter(bidderBid -> Objects.nonNull(bidderBid.getSeat())) + .collect(Collectors.groupingBy(BidderBid::getSeat)); + + if (seatToBids.isEmpty()) { + final BidderSeatBidInfo bidderSeatBidInfo = BidderSeatBidInfo.of( + Collections.emptyList(), + seatBid.getHttpCalls(), + seatBid.getErrors(), + seatBid.getWarnings(), + seatBid.getFledgeAuctionConfigs(), + seatBid.getIgi()); + + result.add(BidderResponseInfo.of( + bidder, + bidder, + bidder, + bidderSeatBidInfo, + bidderResponse.getResponseTime())); + + continue; } - final BidderSeatBidInfo bidderSeatBidInfo = BidderSeatBidInfo.of( - bidInfos, - seatBid.getHttpCalls(), - seatBid.getErrors(), - seatBid.getWarnings(), - seatBid.getFledgeAuctionConfigs()); + for (Map.Entry> bidsEntry : seatToBids.entrySet()) { + final List bidInfos = new ArrayList<>(); + final String seat = bidsEntry.getKey(); + final List bids = bidsEntry.getValue(); + final BidderBid firstBid = CollectionUtils.isEmpty(bids) ? null : bids.getFirst(); + final String adapterCode = Optional.ofNullable(firstBid) + .map(BidderBid::getBid) + .map(Bid::getExt) + .flatMap(ext -> getExtPrebid(ext, ExtBidPrebid.class)) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::getAdapterCode) + .orElse(bidder); + + for (final BidderBid bidderBid : bids) { + final BidInfo bidInfo = toBidInfo( + bidderBid.getBid(), + bidderBid.getType(), + seat, + imps, + bidder, + categoryMappingResult, + cacheInfo, + account); + bidInfos.add(bidInfo); + } - result.add(BidderResponseInfo.of(bidder, bidderSeatBidInfo, bidderResponse.getResponseTime())); - } + final BidderSeatBidInfo bidderSeatBidInfo = BidderSeatBidInfo.of( + bidInfos, + seatBid.getHttpCalls(), + seatBid.getErrors(), + seatBid.getWarnings(), + seatBid.getFledgeAuctionConfigs(), + seatBid.getIgi()); + result.add(BidderResponseInfo.of( + bidder, + seat, + adapterCode, + bidderSeatBidInfo, + bidderResponse.getResponseTime())); + } + } return result; } private BidInfo toBidInfo(Bid bid, BidType type, + String seat, 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) + .seat(seat) .correspondingImp(correspondingImp) - .lineItemId(lineItemId) + .ttl(resolveTtl(bid, type, correspondingImp, cacheInfo, account)) + .vastTtl(type == BidType.video ? resolveVastTtl(bid, correspondingImp, cacheInfo, account) : null) .category(categoryMappingResult.getCategory(bid)) .satisfiedPriority(categoryMappingResult.isBidSatisfiesPriority(bid)) .build(); @@ -451,14 +524,53 @@ private static Optional correspondingImp(String impId, List imps) { .findFirst(); } + private Integer resolveTtl(Bid bid, BidType type, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + final Integer bidTtl = bid.getExp(); + final Integer impTtl = imp != null ? imp.getExp() : null; + final Integer requestTtl = cacheInfo.getCacheBidsTtl(); + + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final Integer accountTtl = accountAuctionConfig != null ? switch (type) { + case banner -> accountAuctionConfig.getBannerCacheTtl(); + case video -> accountAuctionConfig.getVideoCacheTtl(); + case audio, xNative -> null; + } : null; + + final Integer mediaTypeTtl = switch (type) { + case banner -> mediaTypeCacheTtl.getBannerCacheTtl(); + case video -> mediaTypeCacheTtl.getVideoCacheTtl(); + case audio, xNative -> null; + }; + + final Integer defaultTtl = switch (type) { + case banner -> cacheDefaultProperties.getBannerTtl(); + case video -> cacheDefaultProperties.getVideoTtl(); + case audio -> cacheDefaultProperties.getAudioTtl(); + case xNative -> cacheDefaultProperties.getNativeTtl(); + }; + + return ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountTtl, mediaTypeTtl, defaultTtl); + } + + private Integer resolveVastTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + return ObjectUtils.firstNonNull( + bid.getExp(), + imp != null ? imp.getExp() : null, + cacheInfo.getCacheVideoBidsTtl(), + accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null, + mediaTypeCacheTtl.getVideoCacheTtl(), + cacheDefaultProperties.getVideoTtl()); + } + private Future> invokeProcessedBidderResponseHooks(List bidderResponses, AuctionContext auctionContext) { - return CompositeFuture.join(bidderResponses.stream() + return Future.join(bidderResponses.stream() .map(bidderResponse -> hookStageExecutor .executeProcessedBidderResponseStage(bidderResponse, auctionContext) .map(stageResult -> rejectBidderResponseOrProceed(stageResult, bidderResponse))) - .collect(Collectors.toCollection(ArrayList::new))) + .toList()) .map(CompositeFuture::list); } @@ -474,12 +586,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, @@ -488,6 +599,7 @@ private Future createCategoryMapping(AuctionContext aucti return categoryMappingService.createCategoryMapping( bidderResponses, auctionContext.getBidRequest(), + auctionContext.getAccount(), auctionContext.getTimeoutContext().getTimeout()) .map(categoryMappingResult -> addCategoryMappingErrors(categoryMappingResult, auctionContext)); @@ -527,7 +639,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 +647,11 @@ 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 +666,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,113 +692,83 @@ private static boolean preferDeals(ExtRequestTargeting targeting) { private List toBidderResponseWithTargetingBidInfos( List bidderResponses, Map bidderToMultiBids, - boolean preferDeals, - TxnLog txnLog) { - - final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() - .collect(Collectors.toMap( - Function.identity(), - bidderResponse -> toSortedMultiBidInfo(bidderResponse, bidderToMultiBids, preferDeals))); - - final Map>> impIdToBidderToBidInfos = bidderResponseToReducedBidInfos.values() - .stream() - .flatMap(Collection::stream) - .collect(Collectors.groupingBy( - bidInfo -> bidInfo.getCorrespondingImp().getId(), - Collectors.groupingBy(BidInfo::getBidder))); + boolean preferDeals) { - // Best bids from bidders for imp - final Set winningBids = new HashSet<>(); - // All bids from bidder for imp - final Set winningBidsByBidder = new HashSet<>(); + final Comparator comparator = winningBidComparatorFactory.create(preferDeals).reversed(); - for (final Map> bidderToBidInfos : impIdToBidderToBidInfos.values()) { - - bidderToBidInfos.values().forEach(winningBidsByBidder::addAll); - - bidderToBidInfos.values().stream() - .flatMap(Collection::stream) - .max(winningBidComparatorFactory.create(preferDeals)) - .ifPresent(winningBids::add); - } - - final Map> impIdToLineItemIds = impIdToBidderToBidInfos.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - impIdToBidderToBidInfoEntry -> toLineItemIds(impIdToBidderToBidInfoEntry.getValue().values()))); - - updateTopMatchAndLostAuctionLineItemsMetric(winningBids, txnLog, impIdToLineItemIds); + final List> bidInfosPerBidder = bidderResponses.stream() + .map(bidderResponse -> limitMultiBid(bidderResponse, bidderToMultiBids, comparator)) + .toList(); + final List> rankedBidInfos = applyRanking(bidInfosPerBidder, comparator); - return bidderResponseToReducedBidInfos.entrySet().stream() - .map(responseToBidInfos -> injectBidInfoWithTargeting( - responseToBidInfos.getKey(), - responseToBidInfos.getValue(), - bidderToMultiBids, - winningBids, - winningBidsByBidder)) + return IntStream.range(0, bidderResponses.size()) + .mapToObj(i -> enrichBidInfoWithTargeting( + bidderResponses.get(i), + rankedBidInfos.get(i), + bidderToMultiBids)) .toList(); } - private List toSortedMultiBidInfo(BidderResponseInfo bidderResponse, + private static List limitMultiBid(BidderResponseInfo bidderResponse, Map bidderToMultiBids, - boolean preferDeals) { + Comparator comparator) { + + final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); + final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; final List bidInfos = bidderResponse.getSeatBid().getBidsInfos(); final Map> impIdToBidInfos = bidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); - final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); - final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; - return impIdToBidInfos.values().stream() - .map(infos -> sortReducedBidInfo(infos, bidLimit, preferDeals)) - .flatMap(Collection::stream) + .flatMap(infos -> infos.stream() + .sorted(comparator) + .limit(bidLimit)) .toList(); } - private List sortReducedBidInfo(List bidInfos, int limit, boolean preferDeals) { - return bidInfos.stream() - .sorted(winningBidComparatorFactory.create(preferDeals).reversed()) - .limit(limit) - .toList(); - } + private static List> applyRanking(List> bidInfosPerBidder, + Comparator comparator) { - private static Set toLineItemIds(Collection> bidInfos) { - return bidInfos.stream() - .flatMap(Collection::stream) - .map(BidInfo::getLineItemId) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - } + final Map>> impIdToBidderBidInfo = new HashMap<>(); + for (int bidderIndex = 0; bidderIndex < bidInfosPerBidder.size(); bidderIndex++) { + final List bidInfos = bidInfosPerBidder.get(bidderIndex); - /** - * 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)); + for (BidInfo bidInfo : bidInfos) { + impIdToBidderBidInfo + .computeIfAbsent(bidInfo.getCorrespondingImp().getId(), ignore -> new ArrayList<>()) + .add(Pair.of(bidderIndex, bidInfo)); } } + + for (List> bidderToBidInfo : impIdToBidderBidInfo.values()) { + bidderToBidInfo.sort(Comparator.comparing(Pair::getRight, comparator)); + } + + final List> rankedBidInfosPerBidder = new ArrayList<>(); + for (int i = 0; i < bidInfosPerBidder.size(); i++) { + rankedBidInfosPerBidder.add(new ArrayList<>()); + } + + for (List> sortedBidderToBidInfo : impIdToBidderBidInfo.values()) { + for (int rank = 0; rank < sortedBidderToBidInfo.size(); rank++) { + final Pair bidderToBidInfo = sortedBidderToBidInfo.get(rank); + final BidInfo bidInfo = bidderToBidInfo.getRight(); + + rankedBidInfosPerBidder.get(bidderToBidInfo.getLeft()) + .add(bidInfo.toBuilder().rank(rank + 1).build()); + } + } + + return rankedBidInfosPerBidder; } - private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, + private static BidderResponseInfo enrichBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, List bidderBidInfos, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final String bidder = bidderResponseInfo.getBidder(); - final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids, - winningBids, winningBidsByBidder); + final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids); final BidderSeatBidInfo seatBid = bidderResponseInfo.getSeatBid(); final BidderSeatBidInfo modifiedSeatBid = seatBid.with(bidInfosWithTargeting); @@ -696,24 +777,20 @@ private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo private static List toBidInfoWithTargeting(List bidderBidInfos, String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final Map> impIdToBidInfos = bidderBidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); return impIdToBidInfos.values().stream() - .map(bidInfos -> injectTargeting(bidInfos, bidder, bidderToMultiBids, winningBids, winningBidsByBidder)) + .map(bidInfos -> enrichWithTargeting(bidInfos, bidder, bidderToMultiBids)) .flatMap(Collection::stream) .toList(); } - private static List injectTargeting(List bidderImpIdBidInfos, - String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + private static List enrichWithTargeting(List bidderImpIdBidInfos, + String bidder, + Map bidderToMultiBids) { final List result = new ArrayList<>(); @@ -723,18 +800,15 @@ private static List injectTargeting(List bidderImpIdBidInfos, final int multiBidSize = bidderImpIdBidInfos.size(); for (int i = 0; i < multiBidSize; i++) { // first bid have the highest value and can't be extra bid - final boolean isFirstBid = i == 0; - final String targetingBidderCode = isFirstBid - ? bidder - : bidderCodePrefix == null ? null : bidderCodePrefix + (i + 1); - + final String targetingBidderCode = targetingCode(bidder, bidderCodePrefix, i); final BidInfo bidInfo = bidderImpIdBidInfos.get(i); + final TargetingInfo targetingInfo = TargetingInfo.builder() .isTargetingEnabled(targetingBidderCode != null) - .isBidderWinningBid(winningBidsByBidder.contains(bidInfo)) - .isWinningBid(winningBids.contains(bidInfo)) + .isWinningBid(bidInfo.getRank() == 1) .isAddTargetBidderCode(targetingBidderCode != null && multiBidSize > 1) .bidderCode(targetingBidderCode) + .seat(targetingCode(bidInfo.getSeat(), bidderCodePrefix, i)) .build(); final BidInfo modifiedBidInfo = bidInfo.toBuilder().targetingInfo(targetingInfo).build(); @@ -744,20 +818,14 @@ 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)); + private static String targetingCode(String base, String prefix, int i) { + if (i == 0) { + return base; + } + + return prefix != null ? prefix + (i + 1) : null; } - /** - * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) - * from all bidders. - */ private ExtBidResponse toExtBidResponse(List bidderResponseInfos, AuctionContext auctionContext, CacheServiceResult cacheResult, @@ -769,6 +837,10 @@ private ExtBidResponse toExtBidResponse(List bidderResponseI final DebugContext debugContext = auctionContext.getDebugContext(); final boolean debugEnabled = debugContext.isDebugEnabled(); + final PaaResult paaResult = toPaaOutput(bidderResponseInfos, auctionContext); + final List igi = paaResult.igis(); + final ExtBidResponseFledge fledge = paaResult.fledge(); + final ExtResponseDebug extResponseDebug = toExtResponseDebug( bidderResponseInfos, auctionContext, cacheResult, debugEnabled); final Map> errors = toExtBidderErrors( @@ -778,9 +850,8 @@ private ExtBidResponse toExtBidResponse(List bidderResponseI final Map responseTimeMillis = toResponseTimes(bidderResponseInfos, cacheResult); - final ExtBidResponseFledge extBidResponseFledge = toExtBidResponseFledge(bidderResponseInfos, auctionContext); final ExtBidResponsePrebid prebid = toExtBidResponsePrebid( - auctionTimestamp, auctionContext.getBidRequest(), extBidResponseFledge); + auctionTimestamp, auctionContext.getBidRequest(), fledge); return ExtBidResponse.builder() .debug(extResponseDebug) @@ -788,6 +859,7 @@ private ExtBidResponse toExtBidResponse(List bidderResponseI .warnings(warnings) .responsetimemillis(responseTimeMillis) .tmaxrequest(auctionContext.getBidRequest().getTmax()) + .igi(igi) .prebid(prebid) .build(); } @@ -809,22 +881,143 @@ private ExtBidResponsePrebid toExtBidResponsePrebid(long auctionTimestamp, .build(); } - private ExtBidResponseFledge toExtBidResponseFledge(List bidderResponseInfos, - AuctionContext auctionContext) { + private PaaResult toPaaOutput(List bidderResponseInfos, AuctionContext auctionContext) { + + final PaaFormat paaFormat = resolvePaaFormat(auctionContext); + final List igis = extractIgis(bidderResponseInfos, auctionContext); + final List extIgi = paaFormat == PaaFormat.IAB && !igis.isEmpty() ? igis : null; + final List fledgeConfigs = paaFormat == PaaFormat.ORIGINAL + ? toOriginalFledgeFormat(igis) + : Collections.emptyList(); + + // TODO: Remove after transition period final List imps = auctionContext.getBidRequest().getImp(); - final List fledgeConfigs = bidderResponseInfos.stream() - .flatMap(bidderResponseInfo -> fledgeConfigsForBidder(bidderResponseInfo, imps)) + final List deprecatedFledgeConfigs = bidderResponseInfos.stream() + .flatMap(bidderResponseInfo -> toDeprecatedFledgeConfigs(bidderResponseInfo, imps)) + .toList(); + + final List combinedFledgeConfigs = ListUtils.union(deprecatedFledgeConfigs, fledgeConfigs); + final ExtBidResponseFledge extBidResponseFledge = combinedFledgeConfigs.isEmpty() + ? null + : ExtBidResponseFledge.of(combinedFledgeConfigs); + + return new PaaResult(extIgi, extBidResponseFledge); + } + + private List extractIgis(List bidderResponseInfos, AuctionContext auctionContext) { + return bidderResponseInfos.stream() + .flatMap(responseInfo -> responseInfo.getSeatBid().getIgi().stream() + .map(igi -> prepareExtIgi( + igi, + responseInfo.getSeat(), + responseInfo.getAdapterCode(), + auctionContext))) + .filter(Objects::nonNull) + .toList(); + } + + private ExtIgi prepareExtIgi(ExtIgi igi, + String seat, + String adapterCode, + AuctionContext auctionContext) { + if (igi == null) { + return null; + } + + final boolean shouldDropIgb = StringUtils.isEmpty(igi.getImpid()) && CollectionUtils.isNotEmpty(igi.getIgb()); + if (shouldDropIgb) { + final String warning = "ExtIgi with absent impId from bidder: " + seat; + if (auctionContext.getDebugContext().isDebugEnabled()) { + auctionContext.getDebugWarnings().add(warning); + } + conditionalLogger.warn(warning, logSamplingRate); + metrics.updateAlertsMetrics(MetricName.general); + } + + final List updatedIgs = prepareExtIgiIgs(igi.getIgs(), seat, adapterCode, auctionContext); + final List preparedIgs = updatedIgs.isEmpty() ? null : updatedIgs; + final List preparedIgb = shouldDropIgb ? null : igi.getIgb(); + + return ObjectUtils.anyNotNull(preparedIgs, preparedIgb) + ? igi.toBuilder().igs(preparedIgs).igb(preparedIgb).build() + : null; + } + + private List prepareExtIgiIgs(List igiIgs, + String seat, + String adapterCode, + AuctionContext auctionContext) { + + if (igiIgs == null) { + return Collections.emptyList(); + } + + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + final List preparedIgiIgs = new ArrayList<>(); + for (ExtIgiIgs extIgiIgs : igiIgs) { + if (extIgiIgs == null) { + continue; + } + + if (StringUtils.isEmpty(extIgiIgs.getImpId())) { + final String warning = "ExtIgiIgs with absent impId from bidder: " + seat; + if (debugEnabled) { + auctionContext.getDebugWarnings().add(warning); + } + conditionalLogger.warn(warning, logSamplingRate); + metrics.updateAlertsMetrics(MetricName.general); + continue; + } + + if (extIgiIgs.getConfig() == null) { + final String warning = "ExtIgiIgs with absent config from bidder: " + seat; + if (debugEnabled) { + auctionContext.getDebugWarnings().add(warning); + } + conditionalLogger.warn(warning, logSamplingRate); + metrics.updateAlertsMetrics(MetricName.general); + continue; + } + + final ExtIgiIgs preparedExtIgiIgs = extIgiIgs.toBuilder() + .ext(ExtIgiIgsExt.of(seat, adapterCode)) + .build(); + + preparedIgiIgs.add(preparedExtIgiIgs); + } + + return preparedIgiIgs; + } + + private List toOriginalFledgeFormat(List igis) { + return igis.stream() + .map(ExtIgi::getIgs) + .flatMap(Collection::stream) + .map(BidResponseCreator::extIgiIgsToFledgeConfig) .toList(); - return !fledgeConfigs.isEmpty() ? ExtBidResponseFledge.of(fledgeConfigs) : null; } - private Stream fledgeConfigsForBidder(BidderResponseInfo bidderResponseInfo, List imps) { + private static FledgeAuctionConfig extIgiIgsToFledgeConfig(ExtIgiIgs extIgiIgs) { + return FledgeAuctionConfig.builder() + .bidder(extIgiIgs.getExt().getBidder()) + .adapter(extIgiIgs.getExt().getAdapter()) + .impId(extIgiIgs.getImpId()) + .config(extIgiIgs.getConfig()) + .build(); + } + + private Stream toDeprecatedFledgeConfigs(BidderResponseInfo bidderResponseInfo, + List imps) { + return Optional.ofNullable(bidderResponseInfo.getSeatBid().getFledgeAuctionConfigs()) .stream() .flatMap(Collection::stream) .filter(fledgeConfig -> validateFledgeConfig(fledgeConfig, imps)) - .map(fledgeConfig -> fledgeConfigWithBidder(fledgeConfig, bidderResponseInfo.getBidder())); + .map(fledgeConfig -> fledgeConfigWithBidder( + fledgeConfig, + bidderResponseInfo.getSeat(), + bidderResponseInfo.getAdapterCode())); } private boolean validateFledgeConfig(FledgeAuctionConfig fledgeAuctionConfig, List imps) { @@ -836,10 +1029,13 @@ private boolean validateFledgeConfig(FledgeAuctionConfig fledgeAuctionConfig, Li return fledgeEnabled == ExtImpAuctionEnvironment.ON_DEVICE_IG_AUCTION_FLEDGE; } - private static FledgeAuctionConfig fledgeConfigWithBidder(FledgeAuctionConfig fledgeConfig, String bidderName) { + private FledgeAuctionConfig fledgeConfigWithBidder(FledgeAuctionConfig fledgeConfig, + String seat, + String adapterCode) { + return fledgeConfig.toBuilder() - .bidder(bidderName) - .adapter(bidderName) + .bidder(seat) + .adapter(adapterCode) .build(); } @@ -853,13 +1049,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 +1074,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)); } @@ -932,8 +1123,9 @@ private static Map> toExtHttpCalls(List> bidderHttpCalls = bidderResponses.stream() .filter(bidderResponse -> CollectionUtils.isNotEmpty(bidderResponse.getSeatBid().getHttpCalls())) .collect(Collectors.toMap( - BidderResponseInfo::getBidder, - bidderResponse -> bidderResponse.getSeatBid().getHttpCalls())); + BidderResponseInfo::getSeat, + bidderResponse -> bidderResponse.getSeatBid().getHttpCalls(), + ListUtil::union)); final DebugHttpCall httpCall = cacheResult.getHttpCall(); final ExtHttpCall cacheExtHttpCall = httpCall != null ? toExtHttpCall(httpCall) : null; @@ -963,54 +1155,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; } @@ -1021,7 +1175,6 @@ private Map> toExtBidderErrors(List> bidErrors) { final Map> errors = new HashMap<>(); - errors.putAll(extractBidderErrors(bidderResponses)); errors.putAll(extractDeprecatedBiddersErrors(auctionContext.getBidRequest())); errors.putAll(extractPrebidErrors(videoStoredDataResult, auctionContext)); @@ -1029,7 +1182,6 @@ private Map> toExtBidderErrors(List> extractBidderErrors( return bidderResponses.stream() .filter(bidderResponse -> CollectionUtils.isNotEmpty(bidderResponse.getSeatBid().getErrors())) - .collect(Collectors.toMap(BidderResponseInfo::getBidder, - bidderResponse -> errorsDetails(bidderResponse.getSeatBid().getErrors()))); + .collect(Collectors.toMap( + BidderResponseInfo::getSeat, + bidderResponse -> errorsDetails(bidderResponse.getSeatBid().getErrors()), + ListUtil::union)); } /** @@ -1053,8 +1207,10 @@ private static Map> extractBidderWarnings( return bidderResponses.stream() .filter(bidderResponse -> CollectionUtils.isNotEmpty(bidderResponse.getSeatBid().getWarnings())) - .collect(Collectors.toMap(BidderResponseInfo::getBidder, - bidderResponse -> errorsDetails(bidderResponse.getSeatBid().getWarnings()))); + .collect(Collectors.toMap( + BidderResponseInfo::getSeat, + bidderResponse -> errorsDetails(bidderResponse.getSeatBid().getWarnings()), + ListUtil::union)); } /** @@ -1102,7 +1258,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 +1316,7 @@ private static Map> toExtBidderWarnings( List bidderResponses, AuctionContext auctionContext, Map> bidWarnings) { + final Map> warnings = new HashMap<>(); warnings.putAll(extractContextWarnings(auctionContext)); @@ -1176,7 +1333,7 @@ private static Map> extractContextWarnings(AuctionC return contextWarnings.isEmpty() ? Collections.emptyMap() - : Collections.singletonMap(PREBID_EXT, contextWarnings); + : Collections.singletonMap(DEFAULT_DEBUG_KEY, contextWarnings); } /** @@ -1186,7 +1343,7 @@ private static Map toResponseTimes(Collection responseTimeMillis = bidderResponses.stream() - .collect(Collectors.toMap(BidderResponseInfo::getBidder, BidderResponseInfo::getResponseTime)); + .collect(Collectors.toMap(BidderResponseInfo::getSeat, BidderResponseInfo::getResponseTime, Math::max)); final DebugHttpCall debugHttpCall = cacheResult.getHttpCall(); final Integer cacheResponseTime = debugHttpCall != null ? debugHttpCall.getResponseTimeMillis() : null; @@ -1196,6 +1353,17 @@ private static Map toResponseTimes(Collection Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPaaFormat)) + .orElse(PaaFormat.ORIGINAL); + } + /** * Returns {@link BidResponse} based on list of {@link BidderResponse}s and {@link CacheServiceResult}. */ @@ -1244,7 +1412,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(); @@ -1305,12 +1473,6 @@ private SeatBid toSeatBid(List bidInfos, Map> bidErrors, Map> bidWarnings) { - final String bidder = bidInfos.stream() - .map(BidInfo::getBidder) - .findFirst() - // Should never occur - .orElseThrow(() -> new IllegalArgumentException("Bidder was not defined for bidInfo")); - final List bids = bidInfos.stream() .map(bidInfo -> injectAdmWithCacheInfo( bidInfo, @@ -1327,8 +1489,14 @@ private SeatBid toSeatBid(List bidInfos, .filter(Objects::nonNull) .toList(); + final String seat = bidInfos.stream() + .map(BidInfo::getSeat) + .findFirst() + // Should never occur + .orElseThrow(() -> new IllegalArgumentException("BidderCode was not defined for bidInfo")); + return SeatBid.builder() - .seat(bidder) + .seat(seat) .bid(bids) .group(0) // prebid cannot support roadblocking .build(); @@ -1341,7 +1509,7 @@ private BidInfo injectAdmWithCacheInfo(BidInfo bidInfo, final Bid bid = bidInfo.getBid(); final BidType bidType = bidInfo.getBidType(); - final String bidder = bidInfo.getBidder(); + final String seat = bidInfo.getSeat(); final Imp correspondingImp = bidInfo.getCorrespondingImp(); final CacheInfo cacheInfo = bidsWithCacheIds.get(bid); @@ -1358,7 +1526,7 @@ private BidInfo injectAdmWithCacheInfo(BidInfo bidInfo, try { modifiedBidAdm = createNativeMarkup(modifiedBidAdm, correspondingImp); } catch (PreBidException e) { - bidErrors.computeIfAbsent(bidder, ignored -> new ArrayList<>()) + bidErrors.computeIfAbsent(seat, ignored -> new ArrayList<>()) .add(ExtBidderError.of(BidderError.Type.bad_server_response.getCode(), e.getMessage())); return null; } @@ -1379,6 +1547,7 @@ private Bid toBid(BidInfo bidInfo, BidRequest bidRequest, Account account, Map> bidWarnings) { + final TargetingInfo targetingInfo = bidInfo.getTargetingInfo(); final BidType bidType = bidInfo.getBidType(); final Bid bid = bidInfo.getBid(); @@ -1387,19 +1556,25 @@ 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 seat = targetingInfo.getSeat(); final String categoryDuration = bidInfo.getCategory(); targetingKeywords = keywordsCreator != null ? keywordsCreator.makeFor( - bid, bidderCode, isWinningBid, cacheId, bidType.getName(), videoCacheId, categoryDuration) + bid, + seat, + isWinningBid, + cacheId, + bidType.getName(), + videoCacheId, + categoryDuration, + account, + bidWarnings) : null; } else { targetingKeywords = null; @@ -1412,23 +1587,26 @@ private Bid toBid(BidInfo bidInfo, final ObjectNode originalBidExt = bid.getExt(); final Boolean dealsTierSatisfied = bidInfo.getSatisfiedPriority(); + final boolean bidRankingEnabled = isBidRankingEnabled(account); + final ExtBidPrebid updatedExtBidPrebid = getExtPrebid(originalBidExt, ExtBidPrebid.class) .map(ExtBidPrebid::toBuilder) .orElseGet(ExtBidPrebid::builder) .targeting(MapUtils.isNotEmpty(targetingKeywords) ? targetingKeywords : null) - .targetBidderCode(targetingInfo.isAddTargetBidderCode() ? bidderCode : null) + .targetBidderCode(targetingInfo.isAddTargetBidderCode() ? targetingInfo.getBidderCode() : null) .dealTierSatisfied(dealsTierSatisfied) .cache(cache) .passThrough(extractPassThrough(bidInfo.getCorrespondingImp())) + .rank(bidRankingEnabled ? bidInfo.getRank() : null) .build(); final ObjectNode updatedBidExt = 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.getVastTtl())); return bid.toBuilder() .ext(updatedBidExt) .exp(ttl) @@ -1438,7 +1616,6 @@ private Bid toBid(BidInfo bidInfo, private boolean shouldIncludeTargetingInResponse(ExtRequestTargeting targeting, TargetingInfo targetingInfo) { return targeting != null && targetingInfo.isTargetingEnabled() - && targetingInfo.isBidderWinningBid() && (Objects.equals(targeting.getIncludebidderkeys(), true) || Objects.equals(targeting.getIncludewinners(), true) || Objects.equals(targeting.getIncludeformat(), true)); @@ -1451,6 +1628,13 @@ private JsonNode extractPassThrough(Imp imp) { .orElse(null); } + private static boolean isBidRankingEnabled(Account account) { + return Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getRanking) + .map(AccountBidRankingConfig::getEnabled) + .orElse(false); + } + private String createNativeMarkup(String bidAdm, Imp correspondingImp) { final Response nativeMarkup; try { @@ -1577,9 +1761,6 @@ private static boolean eventsAllowedByRequest(AuctionContext auctionContext) { return prebid != null && prebid.getEvents() != null; } - /** - * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. - */ private long auctionTimestamp(AuctionContext auctionContext) { final ExtRequest ext = auctionContext.getBidRequest().getExt(); final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; @@ -1598,36 +1779,29 @@ private static String integrationFrom(AuctionContext auctionContext) { private Events createEvents(String bidder, Account account, String bidId, - EventsContext eventsContext, - String lineItemId) { + EventsContext eventsContext) { - if (!eventsContext.isEnabledForAccount()) { - return null; - } - - 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 +1809,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 +1816,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 +1825,6 @@ private TargetingKeywordsCreator keywordsCreator(ExtRequestTargeting targeting, */ private Map keywordsCreatorByBidType( ExtRequestTargeting targeting, - boolean isApp, BidRequest bidRequest, Account account, Map> bidWarnings) { @@ -1668,21 +1840,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 +1862,6 @@ BidType.xNative, createKeywordsCreator(targeting, isApp, xNative, bidRequest, ac } private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targeting, - boolean isApp, JsonNode priceGranularity, BidRequest bidRequest, Account account, @@ -1698,13 +1869,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, @@ -1712,9 +1890,6 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe resolveKeyPrefix); } - /** - * Returns max targeting keyword length. - */ private int resolveTruncateAttrChars(ExtRequestTargeting targeting, Account account) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); final Integer accountTruncateTargetAttr = @@ -1769,11 +1944,6 @@ private static boolean isCachedDebugEnabled(CachedDebugLog cachedDebugLog) { return cachedDebugLog != null && cachedDebugLog.isEnabled(); } - /** - * Parse {@link JsonNode} to {@link List} of {@link ExtPriceGranularity}. - *

- * Throws {@link PreBidException} in case of errors during decoding price granularity. - */ private ExtPriceGranularity parsePriceGranularity(JsonNode priceGranularity) { try { return mapper.mapper().treeToValue(priceGranularity, ExtPriceGranularity.class); @@ -1788,9 +1958,14 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid return bidResponse; } - final List seatNonBids = auctionContext.getBidRejectionTrackers().entrySet().stream() - .map(entry -> toSeatNonBid(entry.getKey(), entry.getValue())) - .filter(seatNonBid -> !seatNonBid.getNonBid().isEmpty()) + final List seatNonBids = auctionContext.getBidRejectionTrackers().values().stream() + .flatMap(bidRejectionTracker -> bidRejectionTracker.getRejected().stream()) + .collect(Collectors.groupingBy( + Rejection::seat, + Collectors.mapping(entry -> NonBid.of(entry.impId(), entry.reason()), Collectors.toList()))) + .entrySet().stream() + .filter(entry -> !entry.getValue().isEmpty()) + .map(entry -> SeatNonBid.of(entry.getKey(), entry.getValue())) .toList(); final ExtBidResponse updatedExtBidResponse = Optional.ofNullable(bidResponse.getExt()) @@ -1802,17 +1977,6 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid return bidResponse.toBuilder().ext(updatedExtBidResponse).build(); } - private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) { - final List nonBid = bidRejectionTracker.getRejectionReasons().entrySet().stream() - .map(entry -> NonBid.of(entry.getKey(), entry.getValue())) - .toList(); - - return SeatNonBid.of(bidder, nonBid); - } - - /** - * Creates {@link CacheAsset} for the given cache ID. - */ private CacheAsset toCacheAsset(String cacheId) { return CacheAsset.of(cacheAssetUrlTemplate.concat(cacheId), cacheId); } @@ -1824,16 +1988,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. - */ private Optional getExtBidPrebidVideo(ObjectNode bidExt) { return getExtPrebid(bidExt, ExtBidPrebid.class) .map(ExtBidPrebid::getVideo); @@ -1852,4 +2006,7 @@ private T convertValue(JsonNode jsonNode, String key, Class typeClass) { return null; } } + + private record PaaResult(List igis, ExtBidResponseFledge fledge) { + } } diff --git a/src/main/java/org/prebid/server/auction/BidderAliases.java b/src/main/java/org/prebid/server/auction/BidderAliases.java deleted file mode 100644 index b9108f589c1..00000000000 --- a/src/main/java/org/prebid/server/auction/BidderAliases.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.prebid.server.auction; - -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.bidder.BidderCatalog; - -import java.util.Map; -import java.util.Objects; - -/** - * Represents aliases configured for bidders - configuration might come in OpenRTB request but not limited to it. - */ -public class BidderAliases { - - private final Map aliasToBidder; - - private final Map aliasToVendorId; - - private final BidderCatalog bidderCatalog; - - private BidderAliases(Map aliasToBidder, - Map aliasToVendorId, - BidderCatalog bidderCatalog) { - - this.aliasToBidder = MapUtils.emptyIfNull(aliasToBidder); - this.aliasToVendorId = MapUtils.emptyIfNull(aliasToVendorId); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - } - - public static BidderAliases of(Map aliasToBidder, - Map aliasToVendorId, - BidderCatalog bidderCatalog) { - - return new BidderAliases(aliasToBidder, aliasToVendorId, bidderCatalog); - } - - public boolean isAliasDefined(String alias) { - return aliasToBidder.containsKey(alias); - } - - public String resolveBidder(String aliasOrBidder) { - return aliasToBidder.getOrDefault(aliasOrBidder, aliasOrBidder); - } - - public boolean isSame(String bidder1, String bidder2) { - return StringUtils.equalsIgnoreCase(resolveBidder(bidder1), resolveBidder(bidder2)); - } - - public Integer resolveAliasVendorId(String alias) { - return aliasToVendorId.containsKey(alias) - ? aliasToVendorId.get(alias) - : resolveAliasVendorIdViaCatalog(alias); - } - - private Integer resolveAliasVendorIdViaCatalog(String alias) { - final String bidderName = resolveBidder(alias); - return bidderCatalog.isActive(bidderName) ? bidderCatalog.vendorIdByName(bidderName) : null; - } -} 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..63b0f4b6db0 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -0,0 +1,124 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.prebid.server.auction.aliases.BidderAliases; +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.bidadjustments.BidAdjustmentsProcessor; +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.floors.PriceFloorEnforcer; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +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 final ResponseBidValidator responseBidValidator; + private final PriceFloorEnforcer priceFloorEnforcer; + private final BidAdjustmentsProcessor bidAdjustmentsProcessor; + private final DsaEnforcer dsaEnforcer; + + public BidsAdjuster(ResponseBidValidator responseBidValidator, + PriceFloorEnforcer priceFloorEnforcer, + BidAdjustmentsProcessor bidAdjustmentsProcessor, + DsaEnforcer dsaEnforcer) { + + this.responseBidValidator = Objects.requireNonNull(responseBidValidator); + this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor); + this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); + } + + public List validateAndAdjustBids(List auctionParticipations, + AuctionContext auctionContext, + BidderAliases aliases) { + + final BidRequest bidRequest = auctionContext.getBidRequest(); + return auctionParticipations.stream() + .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) + .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids( + auctionParticipation, + bidRequest)) + + .map(auctionParticipation -> priceFloorEnforcer.enforce( + bidRequest, + auctionParticipation, + auctionContext.getAccount(), + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + + .map(auctionParticipation -> dsaEnforcer.enforce( + bidRequest, + 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); + } +} diff --git a/src/main/java/org/prebid/server/auction/CpmRange.java b/src/main/java/org/prebid/server/auction/CpmRange.java index 6f521953524..0310f956025 100644 --- a/src/main/java/org/prebid/server/auction/CpmRange.java +++ b/src/main/java/org/prebid/server/auction/CpmRange.java @@ -1,19 +1,25 @@ package org.prebid.server.auction; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountAuctionBidRoundingMode; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; /** * Class for price operating with rules defined in {@link PriceGranularity} */ public class CpmRange { + public static final String DEFAULT_CPM = "0.0"; + private static final Locale LOCALE = Locale.US; private static final int DEFAULT_PRECISION = 2; @@ -23,13 +29,13 @@ private CpmRange() { /** * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in string format */ - public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity) { - final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity); - return value != null ? format(value, priceGranularity.getPrecision()) : StringUtils.EMPTY; + public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { + final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity, account); + return value != null ? format(value, priceGranularity.getPrecision()) : DEFAULT_CPM; } /** - * Formats {@link BigDecimal} value with a given precision and return it's string representation. + * Formats {@link BigDecimal} value with a given precision and return its string representation. */ public static String format(BigDecimal value, Integer precision) { return numberFormat(ObjectUtils.defaultIfNull(precision, DEFAULT_PRECISION)).format(value); @@ -47,7 +53,7 @@ private static NumberFormat numberFormat(int precision) { * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in {@link BigDecimal} * format */ - public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity) { + public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { if (cpm.compareTo(BigDecimal.ZERO) <= 0) { return null; } @@ -69,14 +75,32 @@ public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceG min = max; } - return increment != null ? calculate(cpm, min, increment) : null; + return increment != null ? calculate(cpm, min, increment, resolveRoundingMode(account)) : null; } - private static BigDecimal calculate(BigDecimal cpm, BigDecimal min, BigDecimal increment) { + private static BigDecimal calculate(BigDecimal cpm, + BigDecimal min, + BigDecimal increment, + RoundingMode roundingMode) { + return cpm .subtract(min) - .divide(increment, 0, RoundingMode.FLOOR) + .divide(increment, 0, roundingMode) .multiply(increment) .add(min); } + + private static RoundingMode resolveRoundingMode(Account account) { + final AccountAuctionBidRoundingMode accountRoundingMode = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidRounding) + .orElse(AccountAuctionBidRoundingMode.DOWN); + + return switch (accountRoundingMode) { + case DOWN -> RoundingMode.FLOOR; + case UP -> RoundingMode.CEILING; + case TRUE -> RoundingMode.HALF_UP; + case TIMESPLIT -> ThreadLocalRandom.current().nextBoolean() ? RoundingMode.FLOOR : RoundingMode.CEILING; + }; + } } diff --git a/src/main/java/org/prebid/server/auction/DsaEnforcer.java b/src/main/java/org/prebid/server/auction/DsaEnforcer.java index ee992fae6ed..e123603b9a0 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; @@ -9,22 +9,39 @@ 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.auction.model.BidRejection; 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 +51,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 +61,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(BidRejection.of(bidderBid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY)); updatedBidderBids.remove(bidderBid); } } @@ -71,9 +98,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/EidPermissionResolver.java b/src/main/java/org/prebid/server/auction/EidPermissionResolver.java new file mode 100644 index 00000000000..6a19fd7c82b --- /dev/null +++ b/src/main/java/org/prebid/server/auction/EidPermissionResolver.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.Eid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EidPermissionResolver { + + private static final String WILDCARD_BIDDER = "*"; + + private static final ExtRequestPrebidDataEidPermissions DEFAULT_RULE = ExtRequestPrebidDataEidPermissions.builder() + .bidders(Collections.singletonList(WILDCARD_BIDDER)) + .build(); + + private static final EidPermissionResolver EMPTY = new EidPermissionResolver(Collections.emptyList()); + + private final List eidPermissions; + + private EidPermissionResolver(List eidPermissions) { + this.eidPermissions = new ArrayList<>(eidPermissions); + this.eidPermissions.add(DEFAULT_RULE); + } + + public static EidPermissionResolver of(List eidPermissions) { + return new EidPermissionResolver(eidPermissions); + } + + public static EidPermissionResolver empty() { + return EMPTY; + } + + public List resolveAllowedEids(List userEids, String bidder) { + return CollectionUtils.emptyIfNull(userEids) + .stream() + .filter(userEid -> isAllowed(userEid, bidder)) + .toList(); + } + + private boolean isAllowed(Eid eid, String bidder) { + final Map> matchingRulesBySpecificity = eidPermissions + .stream() + .filter(rule -> isRuleMatched(eid, rule)) + .collect(Collectors.groupingBy(this::getRuleSpecificity)); + + final int highestSpecificityMatchingRules = Collections.max(matchingRulesBySpecificity.keySet()); + return matchingRulesBySpecificity.get(highestSpecificityMatchingRules).stream() + .anyMatch(eidPermission -> isBidderAllowed(bidder, eidPermission.getBidders())); + } + + private int getRuleSpecificity(ExtRequestPrebidDataEidPermissions eidPermission) { + return (int) Stream.of( + eidPermission.getInserter(), + eidPermission.getSource(), + eidPermission.getMatcher(), + eidPermission.getMm()) + .filter(Objects::nonNull) + .count(); + } + + private boolean isRuleMatched(Eid eid, ExtRequestPrebidDataEidPermissions eidPermission) { + return (eidPermission.getInserter() == null || eidPermission.getInserter().equals(eid.getInserter())) + && (eidPermission.getSource() == null || eidPermission.getSource().equals(eid.getSource())) + && (eidPermission.getMatcher() == null || eidPermission.getMatcher().equals(eid.getMatcher())) + && (eidPermission.getMm() == null || eidPermission.getMm().equals(eid.getMm())); + } + + private boolean isBidderAllowed(String bidder, List ruleBidders) { + return ruleBidders == null || ruleBidders.stream() + .anyMatch(allowedBidder -> StringUtils.equalsIgnoreCase(allowedBidder, bidder) + || WILDCARD_BIDDER.equals(allowedBidder)); + } +} diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 43b2515c2a6..428510be069 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -1,14 +1,11 @@ package org.prebid.server.auction; -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.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; @@ -21,22 +18,22 @@ 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; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; 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.aliases.AlternateBidderCodesConfig; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPostProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestRejectedException; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidRejectionReason; @@ -47,8 +44,8 @@ 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; @@ -58,35 +55,23 @@ 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.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.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; -import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookExecutionOutcome; -import org.prebid.server.hooks.execution.model.HookId; 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; @@ -95,48 +80,32 @@ import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtBidderConfigOrtb; import org.prebid.server.proto.openrtb.ext.request.ExtDooh; -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.ExtRequestPrebidAlternateBidderCodes; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; 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.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.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountCacheConfig; +import org.prebid.server.util.BidderUtil; 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; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -145,56 +114,45 @@ 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); private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private static final String PREBID_EXT = "prebid"; + private static final String PREBID_META_EXT = "meta"; 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; - private static final String EID_ALLOWED_FOR_ALL_BIDDERS = "*"; private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000); + private static final Set BIDDER_FIELDS_EXCEPTION_LIST = Set.of( + "adunitcode", "storedrequest", "options", "is_rewarded_inventory"); 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; + private final BidderRequestPostProcessor bidderRequestPostProcessor; private final UidUpdater uidUpdater; private final TimeoutResolver timeoutResolver; 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,28 +162,24 @@ 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, + BidderRequestPostProcessor bidderRequestPostProcessor, UidUpdater uidUpdater, TimeoutResolver timeoutResolver, 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,28 +189,24 @@ 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); + this.bidderRequestPostProcessor = Objects.requireNonNull(bidderRequestPostProcessor); this.uidUpdater = Objects.requireNonNull(uidUpdater); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); 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,15 +214,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(this::updateHooksMetrics); + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo); } private Future processAuctionRequest(AuctionContext context) { @@ -294,23 +240,24 @@ private Future runAuction(AuctionContext receivedContext) { final MetricName requestTypeMetric = receivedContext.getRequestTypeMetric(); final List storedAuctionResponses = new ArrayList<>(); - final BidderAliases aliases = aliases(bidRequest); - final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest); + final BidderAliases aliases = aliases(bidRequest, account); + final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest, account); final Map bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings); - receivedContext.getBidRejectionTrackers().putAll(makeBidRejectionTrackers(bidRequest, aliases)); + + populateBidRejectionTrackers(receivedContext, aliases); + + final boolean debugEnabled = receivedContext.getDebugContext().isDebugEnabled(); + metrics.updateDebugRequestMetrics(debugEnabled); + metrics.updateAccountDebugRequestMetrics(account, debugEnabled); 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( + .compose(context -> Future.join( context.getAuctionParticipations().stream() .map(auctionParticipation -> processAndRequestBids( context, @@ -318,47 +265,65 @@ private Future runAuction(AuctionContext receivedContext) { timeout, aliases) .map(auctionParticipation::with)) - .collect(Collectors.toCollection(ArrayList::new))) + .toList()) // send all the requests to the bidders and gathers results .map(CompositeFuture::list) .map(storedResponseProcessor::updateStoredBidResponse) .map(auctionParticipations -> storedResponseProcessor.mergeWithBidderResponses( - auctionParticipations, storedAuctionResponses, bidRequest.getImp())) - .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) - .map(auctionParticipations -> validateAndAdjustBids(auctionParticipations, context, aliases)) + auctionParticipations, + storedAuctionResponses, + bidRequest.getImp(), + context.getBidRejectionTrackers())) + .map(auctionParticipations -> dropZeroNonDealBids( + auctionParticipations, debugWarnings, debugEnabled)) + .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(), + debugEnabled)) .compose(bidResponse -> bidResponsePostProcessor.postProcess( context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account)) .map(context::with)); } - private BidderAliases aliases(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + private BidderAliases aliases(BidRequest bidRequest, Account account) { + 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); + final ExtRequestPrebidAlternateBidderCodes alternateBidderCodes = prebid != null + ? prebid.getAlternateBidderCodes() + : null; + + final AlternateBidderCodesConfig alternateBidderCodesConfig = ObjectUtils.defaultIfNull( + alternateBidderCodes, + account.getAlternateBidderCodes()); + + return BidderAliases.of(aliases, aliasgvlids, bidderCatalog, alternateBidderCodesConfig); } 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) { + private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest, Account account) { + final boolean cachingEnabled = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getCache) + .map(AccountCacheConfig::getEnabled) + .orElse(true); + 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) { + if (cachingEnabled && targeting != null && cache != null) { final boolean shouldCacheBids = cache.getBids() != null; final boolean shouldCacheVideoBids = cache.getVastxml() != null; final boolean shouldCacheWinningBidsOnly = !targeting.getIncludebidderkeys() @@ -391,13 +356,8 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { 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 +370,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; } @@ -462,7 +421,29 @@ private static MultiBidConfig toMultiBid(String bidder, Integer maxBids, String return MultiBidConfig.of(bidder, bidLimit, codePrefix); } - private Map makeBidRejectionTrackers(BidRequest bidRequest, BidderAliases aliases) { + private void populateBidRejectionTrackers(AuctionContext auctionContext, BidderAliases aliases) { + final Map bidRejectionTrackers = auctionContext.getBidRejectionTrackers(); + removeInvalidBidRejectionTrackers(bidRejectionTrackers, aliases); + makeBidRejectionTrackers(bidRejectionTrackers, auctionContext.getBidRequest(), aliases); + } + + private void removeInvalidBidRejectionTrackers(Map bidRejectionTrackers, + BidderAliases aliases) { + + final Set bidderNames = new HashSet<>(bidRejectionTrackers.keySet()); + for (String bidder : bidderNames) { + if (!isValidBidder(bidder, aliases)) { + bidRejectionTrackers.remove(bidder); + logger.warn("Pre-rejected impressions of the bidder {} have been removed. " + + "Reason: the bidder is invalid"); + } + } + } + + private void makeBidRejectionTrackers(Map bidRejectionTrackers, + BidRequest bidRequest, + BidderAliases aliases) { + final Map> impIdToBidders = bidRequest.getImp().stream() .filter(Objects::nonNull) .filter(imp -> StringUtils.isNotEmpty(imp.getId())) @@ -476,49 +457,21 @@ private Map makeBidRejectionTrackers(BidRequest bid bidderToImpIds.computeIfAbsent(bidder, bidderName -> new HashSet<>()).add(impId)); } - return bidderToImpIds.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getKey, - entry -> new BidRejectionTracker(entry.getKey(), entry.getValue(), logSamplingRate))); + bidderToImpIds.forEach((bidder, impIds) -> { + if (bidRejectionTrackers.containsKey(bidder)) { + bidRejectionTrackers.put(bidder, new BidRejectionTracker(bidRejectionTrackers.get(bidder), impIds)); + } else { + bidRejectionTrackers.put(bidder, new BidRejectionTracker(bidder, impIds, 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, @@ -535,11 +488,14 @@ private Future> extractAuctionParticipations( .filter(bidder -> isBidderCallActivityAllowed(bidder, context)) .distinct() .toList(); - final Map> impBidderToStoredBidResponse = - storedResponseResult.getImpBidderToStoredBidResponse(); - return makeAuctionParticipation(bidders, context, aliases, impBidderToStoredBidResponse, - imps, bidderToMultiBid); + return makeAuctionParticipation( + bidders, + context, + aliases, + storedResponseResult.getImpBidderToStoredBidResponse(), + imps, + bidderToMultiBid); } private Set bidderNamesFromImpExt(Imp imp, BidderAliases aliases) { @@ -553,9 +509,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 +524,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, @@ -596,16 +534,22 @@ private Future> makeAuctionParticipation( final BidRequest bidRequest = context.getBidRequest(); final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; final Map biddersToConfigs = getBiddersToConfigs(prebid); - final Map> eidPermissions = getEidPermissions(prebid); - final Map bidderToUser = - prepareUsers(bidders, context, aliases, biddersToConfigs, eidPermissions); - - return privacyEnforcementService.mask(context, bidderToUser, aliases) - .map(bidderToPrivacyResult -> - getAuctionParticipation(bidderToPrivacyResult, bidRequest, impBidderToStoredResponse, imps, - bidderToMultiBid, biddersToConfigs, aliases, context)); + final EidPermissionResolver eidPermissionResolver = getEidPermissions(prebid); + final Map> bidderToUserAndDevice = + prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissionResolver); + + return privacyEnforcementService.mask(context, bidderToUserAndDevice, aliases) + .map(bidderToPrivacyResult -> getAuctionParticipation( + bidderToPrivacyResult, + bidRequest, + impBidderToStoredResponse, + imps, + bidderToMultiBid, + biddersToConfigs, + aliases, + context)); } private Map getBiddersToConfigs(ExtRequestPrebid prebid) { @@ -632,70 +576,57 @@ 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 - ? prebidData.getEidPermissions() - : null; - return CollectionUtils.emptyIfNull(eidPermissions).stream() - .collect(Collectors.toMap(ExtRequestPrebidDataEidPermissions::getSource, - ExtRequestPrebidDataEidPermissions::getBidders)); + private EidPermissionResolver getEidPermissions(ExtRequestPrebid prebid) { + return Optional.ofNullable(prebid) + .map(ExtRequestPrebid::getData) + .map(ExtRequestPrebidData::getEidPermissions) + .map(EidPermissionResolver::of) + .orElse(EidPermissionResolver.empty()); } - /** - * 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(); return data == null ? null : data.getBidders(); } - private Map prepareUsers(List bidders, - AuctionContext context, - BidderAliases aliases, - Map biddersToConfigs, - Map> eidPermissions) { + private Map> prepareUsersAndDevices( + List bidders, + AuctionContext context, + BidderAliases aliases, + Map biddersToConfigs, + EidPermissionResolver eidPermissionResolver) { final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); - final Map bidderToUser = new HashMap<>(); + final Map> bidderToUserAndDevice = new HashMap<>(); for (String bidder : bidders) { final ExtBidderConfigOrtb fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() .anyMatch(fpdBidder -> StringUtils.equalsIgnoreCase(fpdBidder, bidder)); final User preparedUser = prepareUser( - bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissions); - bidderToUser.put(bidder, preparedUser); + bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissionResolver); + final Device preparedDevice = prepareDevice( + bidRequest.getDevice(), fpdConfig, useFirstPartyData); + bidderToUserAndDevice.put(bidder, Pair.of(preparedUser, preparedDevice)); } - return bidderToUser; + return bidderToUserAndDevice; } - /** - * 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, boolean useFirstPartyData, ExtBidderConfigOrtb fpdConfig, - Map> eidPermissions) { + EidPermissionResolver eidPermissionResolver) { final User user = context.getBidRequest().getUser(); final ExtUser extUser = user != null ? user.getExt() : null; final UpdateResult buyerUidUpdateResult = uidUpdater.updateUid(bidder, context, aliases); final List userEids = extractUserEids(user); - final List allowedUserEids = resolveAllowedEids(userEids, bidder, eidPermissions); + final List allowedUserEids = eidPermissionResolver.resolveAllowedEids(userEids, bidder); final boolean shouldUpdateUserEids = allowedUserEids.size() != CollectionUtils.emptyIfNull(userEids).size(); final boolean shouldCleanExtPrebid = extUser != null && extUser.getPrebid() != null; final boolean shouldCleanExtData = extUser != null && extUser.getData() != null && !useFirstPartyData; @@ -708,7 +639,7 @@ private User prepareUser(String bidder, userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { - userBuilder.eids(nullIfEmpty(allowedUserEids)); + userBuilder.eids(ListUtil.nullIfEmpty(allowedUserEids)); } if (shouldUpdateUserExt) { @@ -735,30 +666,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() - .filter(userEid -> isUserEidAllowed(userEid.getSource(), eidPermissions, bidder)) - .toList(); - } - - /** - * Returns true if {@param source} allowed by {@param eidPermissions} for particular bidder taking into account - * ealiases. - */ - private boolean isUserEidAllowed(String source, Map> eidPermissions, String bidder) { - final List allowedBidders = eidPermissions.get(source); - return CollectionUtils.isEmpty(allowedBidders) || allowedBidders.stream() - .anyMatch(allowedBidder -> StringUtils.equalsIgnoreCase(allowedBidder, bidder) - || 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 +702,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 +718,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 +734,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,21 +746,35 @@ 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 enrichedWithPriceFloors = priceFloorProcessor.enrichWithPriceFloors( + context.getBidRequest().toBuilder().imp(imps).build(), + context.getAccount(), + bidder, + context.getPrebidErrors(), + context.getDebugWarnings()); + final BidRequest preparedBidRequest = prepareBidRequest( bidderPrivacyResult, - imps, + enrichedWithPriceFloors, bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, context); + final Map originalPriceFloors = enrichedWithPriceFloors.getImp().stream() + .filter(imp -> BidderUtil.isValidPrice(imp.getBidfloor()) + && StringUtils.isNotBlank(imp.getBidfloorcur())) + .collect(Collectors.toMap(Imp::getId, imp -> Price.of(imp.getBidfloorcur(), imp.getBidfloor()))); + final BidderRequest bidderRequest = BidderRequest.builder() .bidder(bidder) .ortbVersion(ortbVersion) .storedResponse(storedBidResponse) .bidRequest(preparedBidRequest) + .originalPriceFloors(originalPriceFloors) .build(); return AuctionParticipation.builder() @@ -872,13 +790,12 @@ private OrtbVersion bidderSupportedOrtbVersion(String bidder, BidderAliases alia } private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, - List imps, + BidRequest bidRequest, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, AuctionContext context) { - final BidRequest bidRequest = context.getBidRequest(); final String bidder = bidderPrivacyResult.getRequestBidder(); final boolean transmitTid = transmitTransactionId(bidder, context); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); @@ -924,12 +841,19 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final boolean isApp = preparedApp != null; final boolean isDooh = !isApp && preparedDooh != null; final boolean isSite = !isApp && !isDooh && preparedSite != null; + final List preparedImps = prepareImps( + bidder, + bidRequest, + transmitTid, + useFirstPartyData, + context.getAccount(), + 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 +879,18 @@ 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, + 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, debugWarnings)) + .map(imp -> prepareImp(imp, bidder, bidRequest, transmitTid, useFirstPartyData, account, debugWarnings)) .toList(); } @@ -977,86 +899,61 @@ 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(), 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 impExtPrebid = prepareImpExt(impExt.get(PREBID_EXT), adjustedFloor); + final JsonNode bidderNode = bidderParamsFromImpExt(impExt).get(bidder); + final JsonNode impExtPrebid = cleanUpImpExtPrebid(impExt.get(PREBID_EXT)); 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) { - if (extImpPrebidNode.size() <= 1) { + private JsonNode cleanUpImpExtPrebid(JsonNode extImpPrebid) { + if (extImpPrebid.size() <= 1) { return null; } - final ExtImpPrebid extImpPrebid = extImpPrebid(extImpPrebidNode); - final ExtImpPrebidFloors floors = extImpPrebid.getFloors(); - final ExtImpPrebidFloors updatedFloors = floors != null - ? ExtImpPrebidFloors.of(floors.getFloorRule(), - floors.getFloorRuleValue(), - adjustedFloor, - floors.getFloorMin(), - floors.getFloorMinCur()) - : null; + final Iterator fieldsIterator = extImpPrebid.fieldNames(); + final ObjectNode modifiedExtImpPrebid = extImpPrebid.deepCopy(); - return mapper.mapper().valueToTree(extImpPrebid(extImpPrebidNode).toBuilder() - .floors(updatedFloors) - .bidder(null) - .build()); - } - - /** - * Returns {@link ExtImpPrebid} from imp.ext.prebid {@link JsonNode}. - */ - private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); + while (fieldsIterator.hasNext()) { + final String fieldName = fieldsIterator.next(); + if (!BIDDER_FIELDS_EXCEPTION_LIST.contains(fieldName)) { + modifiedExtImpPrebid.remove(fieldName); + } } + + return modifiedExtImpPrebid; } - /** - * 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; @@ -1073,15 +970,18 @@ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { return useFirstPartyData ? fpdResolver.resolveApp(maskedApp, fpdApp) : maskedApp; } + private Device prepareDevice(Device device, ExtBidderConfigOrtb fpdConfig, boolean useFirstPartyData) { + if (fpdConfig == null) { + return device; + } + return useFirstPartyData ? fpdResolver.resolveDevice(device, fpdConfig.getDevice()) : device; + } + private static ExtApp maskExtApp(ExtApp appExt) { final ExtApp maskedExtApp = ExtApp.of(appExt.getPrebid(), null); 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 +1003,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 +1024,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 +1041,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 +1096,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,60 +1132,42 @@ 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, BidderAliases aliases) { - final String bidderName = bidderRequest.getBidder(); - final MediaTypeProcessingResult mediaTypeProcessingResult = mediaTypeProcessor.process( - bidderRequest.getBidRequest(), bidderName, aliases, auctionContext.getAccount()); + return bidderRequestPostProcessor.process(bidderRequest, aliases, auctionContext) + .compose(result -> invokeHooksAndRequestBids(auctionContext, result.getValue(), timeout, aliases) + .map(response -> response.with(addWarnings(response.getSeatBid(), result.getErrors())))) + .recover(throwable -> recoverBidderRequestRejection( + auctionContext, bidderRequest.getBidder(), throwable)); + } - final List mediaTypeProcessingErrors = mediaTypeProcessingResult.getErrors(); - if (mediaTypeProcessingResult.isRejected()) { + private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { + return CollectionUtils.isNotEmpty(warnings) + ? seatBid.toBuilder() + .warnings(ListUtil.union(warnings, seatBid.getWarnings())) + .build() + : seatBid; + } + + private static Future recoverBidderRequestRejection(AuctionContext auctionContext, + String bidderName, + Throwable throwable) { + + if (throwable instanceof BidderRequestRejectedException rejection) { auctionContext.getBidRejectionTrackers() .get(bidderName) - .rejectAll(BidRejectionReason.REJECTED_BY_MEDIA_TYPE); + .rejectAll(rejection.getRejectionReason()); final BidderSeatBid bidderSeatBid = BidderSeatBid.builder() - .warnings(mediaTypeProcessingErrors) + .warnings(rejection.getErrors()) .build(); + return Future.succeededFuture(BidderResponse.of(bidderName, bidderSeatBid, 0)); } - return Future.succeededFuture(mediaTypeProcessingResult.getBidRequest()) - .map(bidderRequest::with) - .compose(modifiedBidderRequest -> invokeHooksAndRequestBids( - auctionContext, modifiedBidderRequest, timeout, aliases)) - .map(bidderResponse -> bidderResponse.with( - addWarnings(bidderResponse.getSeatBid(), mediaTypeProcessingErrors))); - } - - private static BidderSeatBid addWarnings(BidderSeatBid seatBid, List warnings) { - return CollectionUtils.isNotEmpty(warnings) - ? seatBid.toBuilder() - .warnings(ListUtils.union(warnings, seatBid.getWarnings())) - .build() - : seatBid; + return Future.failedFuture(throwable); } private Future invokeHooksAndRequestBids(AuctionContext auctionContext, @@ -1358,7 +1194,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)); } @@ -1382,6 +1218,7 @@ private Future requestBids(BidderRequest bidderRequest, final String bidderName = bidderRequest.getBidder(); final String resolvedBidderName = aliases.resolveBidder(bidderName); final Bidder bidder = bidderCatalog.bidderByName(resolvedBidderName); + final long bidderTmaxDeductionMs = bidderCatalog.bidderInfoByName(resolvedBidderName).getTmaxDeductionMs(); final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidderName); final TimeoutContext timeoutContext = auctionContext.getTimeoutContext(); @@ -1390,7 +1227,8 @@ private Future requestBids(BidderRequest bidderRequest, final long bidderRequestStartTime = clock.millis(); return Future.succeededFuture(bidderRequest.getBidRequest()) - .map(bidRequest -> adjustTmax(bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime)) + .map(bidRequest -> adjustTmax( + bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime, bidderTmaxDeductionMs)) .map(bidRequest -> ortbVersionConversionManager.convertFromAuctionSupportedVersion( bidRequest, bidderRequest.getOrtbVersion())) .map(bidderRequest::with) @@ -1402,12 +1240,53 @@ private Future requestBids(BidderRequest bidderRequest, requestHeaders, aliases, debugResolver.resolveDebugForBidder(auctionContext, resolvedBidderName))) + .map(seatBid -> populateBidderCode(seatBid, bidderName, resolvedBidderName)) .map(seatBid -> BidderResponse.of(bidderName, seatBid, responseTime(bidderRequestStartTime))); } - private BidRequest adjustTmax(BidRequest bidRequest, long startTime, int adjustmentFactor, long currentTime) { + private BidderSeatBid populateBidderCode(BidderSeatBid seatBid, String bidderName, String resolvedBidderName) { + return seatBid.with(seatBid.getBids().stream() + .map(bidderBid -> bidderBid.toBuilder() + .seat(ObjectUtils.defaultIfNull(bidderBid.getSeat(), bidderName)) + .bid(bidderBid.getBid().toBuilder() + .ext(prepareBidExt( + bidderBid.getBid().getExt(), + bidderCatalog.configuredName(resolvedBidderName))) + .build()) + .build()) + .toList()); + } + + private ObjectNode prepareBidExt(ObjectNode bidExt, String bidderName) { + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + final ObjectNode extPrebid = objectNodeFromOrNew(updatedBidExt, PREBID_EXT); + final ObjectNode extPrebidMeta = objectNodeFromOrNew(extPrebid, PREBID_META_EXT); + + final ObjectNode newData = mapper.mapper().valueToTree( + ExtBidPrebidMeta.builder().adapterCode(bidderName).build()); + extPrebidMeta.setAll(newData); + + return updatedBidExt; + } + + private ObjectNode objectNodeFromOrNew(ObjectNode parent, String key) { + final JsonNode childNode = parent.get(key); + return childNode == null || !childNode.isObject() + ? parent.putObject(key) + : (ObjectNode) childNode; + } + + private BidRequest adjustTmax(BidRequest bidRequest, + long startTime, + int adjustmentFactor, + long currentTime, + long bidderTmaxDeductionMs) { + final long tmax = timeoutResolver.limitToMax(bidRequest.getTmax()); - final long adjustedTmax = timeoutResolver.adjustForBidder(tmax, adjustmentFactor, currentTime - startTime); + final long adjustedTmax = timeoutResolver.adjustForBidder( + tmax, adjustmentFactor, currentTime - startTime, bidderTmaxDeductionMs); + return tmax != adjustedTmax ? bidRequest.toBuilder().tmax(adjustedTmax).build() : bidRequest; @@ -1430,15 +1309,18 @@ private BidderResponse rejectBidderResponseOrProceed(HookStageExecutionResult dropZeroNonDealBids(List auctionParticipations, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { return auctionParticipations.stream() - .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings)) + .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings, isDebugEnabled)) .toList(); } private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionParticipation, - List debugWarnings) { + List debugWarnings, + boolean isDebugEnabled) { + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); final BidderSeatBid seatBid = bidderResponse.getSeatBid(); final List bidderBids = seatBid.getBids(); @@ -1448,8 +1330,11 @@ private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionPar final Bid bid = bidderBid.getBid(); if (isZeroNonDealBids(bid.getPrice(), bid.getDealid())) { metrics.updateAdapterRequestErrorMetric(bidderResponse.getBidder(), MetricName.unknown_error); - debugWarnings.add("Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" - .formatted(bid.getId())); + if (isDebugEnabled) { + debugWarnings.add( + "Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'" + .formatted(bid.getId())); + } } else { validBids.add(bidderBid); } @@ -1466,233 +1351,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 +1378,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,255 +1410,7 @@ 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(); - - final Account account = context.getAccount(); - - stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - stageOutcomes.values().stream() - .flatMap(Collection::stream) - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .collect(Collectors.groupingBy( - outcome -> outcome.getHookId().getModuleCode(), - Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) - .forEach((moduleCode, executionTime) -> - metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); - } - - return context; - } - - private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { - stageOutcomes.stream() - .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) - .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) - .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); - } - - private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { - final HookId hookId = hookOutcome.getHookId(); - final ExecutionStatus status = hookOutcome.getStatus(); - final ExecutionAction action = hookOutcome.getAction(); - final String moduleCode = hookId.getModuleCode(); - - metrics.updateHooksMetrics( - moduleCode, - stage, - hookId.getHookImplCode(), - status, - hookOutcome.getExecutionTime(), - action); - - // account might be null if request is rejected by the entrypoint hook - if (account != null) { - 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/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java index e1e4fa1e436..5e91e38a2de 100644 --- a/src/main/java/org/prebid/server/auction/FpdResolver.java +++ b/src/main/java/org/prebid/server/auction/FpdResolver.java @@ -1,29 +1,21 @@ package org.prebid.server.auction; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; -import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; -import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; -import org.prebid.server.proto.openrtb.ext.request.ExtApp; -import org.prebid.server.proto.openrtb.ext.request.ExtDooh; -import org.prebid.server.proto.openrtb.ext.request.ExtSite; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import java.util.Collections; -import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; public class FpdResolver { @@ -32,18 +24,10 @@ public class FpdResolver { private static final String BIDDERS = "bidders"; private static final String APP = "app"; private static final String DOOH = "dooh"; - private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, BIDDERS); - private static final String EXT = "ext"; + private static final String DEVICE = "device"; + private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, DEVICE, BIDDERS); private static final String CONTEXT = "context"; private static final String DATA = "data"; - private static final Set USER_DATA_ATTR = Collections.singleton("geo"); - private static final Set APP_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy"); - private static final Set SITE_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy", "mobile"); - private static final Set DOOH_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy"); - - private static final TypeReference> USER_DATA_TYPE_REFERENCE = - new TypeReference<>() { - }; private final JacksonMapper jacksonMapper; private final JsonMerger jsonMerger; @@ -54,146 +38,41 @@ public FpdResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) { } public User resolveUser(User originUser, ObjectNode fpdUser) { - if (fpdUser == null) { - return originUser; - } - final User resultUser = originUser == null ? User.builder().build() : originUser; - final ExtUser resolvedExtUser = resolveUserExt(fpdUser, resultUser); - return resultUser.toBuilder() - .keywords(ObjectUtils.defaultIfNull(getString(fpdUser, "keywords"), resultUser.getKeywords())) - .gender(ObjectUtils.defaultIfNull(getString(fpdUser, "gender"), resultUser.getGender())) - .yob(ObjectUtils.defaultIfNull(getInteger(fpdUser, "yob"), resultUser.getYob())) - .data(ObjectUtils.defaultIfNull(getFpdUserData(fpdUser), resultUser.getData())) - .ext(resolvedExtUser) - .build(); - } - - private ExtUser resolveUserExt(ObjectNode fpdUser, User originUser) { - final ExtUser originExtUser = originUser.getExt(); - final ObjectNode resolvedData = - mergeExtData(fpdUser.path(EXT).path(DATA), originExtUser != null ? originExtUser.getData() : null); - - return updateUserExtDataWithFpdAttr(fpdUser, originExtUser, resolvedData); - } - - private ExtUser updateUserExtDataWithFpdAttr(ObjectNode fpdUser, ExtUser originExtUser, ObjectNode extData) { - final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode(); - USER_DATA_ATTR.forEach(attribute -> setAttr(fpdUser, resultData, attribute)); - return originExtUser != null - ? originExtUser.toBuilder().data(resultData.isEmpty() ? null : resultData).build() - : resultData.isEmpty() ? null : ExtUser.builder().data(resultData).build(); - } - - private List getFpdUserData(ObjectNode fpdUser) { - final ArrayNode fpdUserDataNode = getValueFromJsonNode( - fpdUser, DATA, node -> (ArrayNode) node, JsonNode::isArray); - - return toList(fpdUserDataNode, USER_DATA_TYPE_REFERENCE); + return mergeFpd(originUser, fpdUser, User.class); } public App resolveApp(App originApp, ObjectNode fpdApp) { - if (fpdApp == null) { - return originApp; - } - final App resultApp = originApp == null ? App.builder().build() : originApp; - final ExtApp resolvedExtApp = resolveAppExt(fpdApp, resultApp); - return resultApp.toBuilder() - .name(ObjectUtils.defaultIfNull(getString(fpdApp, "name"), resultApp.getName())) - .bundle(ObjectUtils.defaultIfNull(getString(fpdApp, "bundle"), resultApp.getBundle())) - .storeurl(ObjectUtils.defaultIfNull(getString(fpdApp, "storeurl"), resultApp.getStoreurl())) - .domain(ObjectUtils.defaultIfNull(getString(fpdApp, "domain"), resultApp.getDomain())) - .cat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "cat"), resultApp.getCat())) - .sectioncat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "sectioncat"), resultApp.getSectioncat())) - .pagecat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "pagecat"), resultApp.getPagecat())) - .keywords(ObjectUtils.defaultIfNull(getString(fpdApp, "keywords"), resultApp.getKeywords())) - .ext(resolvedExtApp) - .build(); - } - - private ExtApp resolveAppExt(ObjectNode fpdApp, App originApp) { - final ExtApp originExtApp = originApp.getExt(); - final ObjectNode resolvedData = - mergeExtData(fpdApp.path(EXT).path(DATA), originExtApp != null ? originExtApp.getData() : null); - - return updateAppExtDataWithFpdAttr(fpdApp, originExtApp, resolvedData); - } - - private ExtApp updateAppExtDataWithFpdAttr(ObjectNode fpdApp, ExtApp originExtApp, ObjectNode extData) { - final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode(); - APP_DATA_ATTR.forEach(attribute -> setAttr(fpdApp, resultData, attribute)); - return originExtApp != null - ? ExtApp.of(originExtApp.getPrebid(), resultData.isEmpty() ? null : resultData) - : resultData.isEmpty() ? null : ExtApp.of(null, resultData); + return mergeFpd(originApp, fpdApp, App.class); } public Site resolveSite(Site originSite, ObjectNode fpdSite) { - if (fpdSite == null) { - return originSite; - } - final Site resultSite = originSite == null ? Site.builder().build() : originSite; - final ExtSite resolvedExtSite = resolveSiteExt(fpdSite, resultSite); - return resultSite.toBuilder() - .name(ObjectUtils.defaultIfNull(getString(fpdSite, "name"), resultSite.getName())) - .domain(ObjectUtils.defaultIfNull(getString(fpdSite, "domain"), resultSite.getDomain())) - .cat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "cat"), resultSite.getCat())) - .sectioncat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "sectioncat"), resultSite.getSectioncat())) - .pagecat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "pagecat"), resultSite.getPagecat())) - .page(ObjectUtils.defaultIfNull(getString(fpdSite, "page"), resultSite.getPage())) - .keywords(ObjectUtils.defaultIfNull(getString(fpdSite, "keywords"), resultSite.getKeywords())) - .ref(ObjectUtils.defaultIfNull(getString(fpdSite, "ref"), resultSite.getRef())) - .search(ObjectUtils.defaultIfNull(getString(fpdSite, "search"), resultSite.getSearch())) - .ext(resolvedExtSite) - .build(); + return mergeFpd(originSite, fpdSite, Site.class); } - private ExtSite resolveSiteExt(ObjectNode fpdSite, Site originSite) { - final ExtSite originExtSite = originSite.getExt(); - final ObjectNode resolvedData = - mergeExtData(fpdSite.path(EXT).path(DATA), originExtSite != null ? originExtSite.getData() : null); - - return updateSiteExtDataWithFpdAttr(fpdSite, originExtSite, resolvedData); + public Dooh resolveDooh(Dooh originDooh, ObjectNode fpdDooh) { + return mergeFpd(originDooh, fpdDooh, Dooh.class); } - private ExtSite updateSiteExtDataWithFpdAttr(ObjectNode fpdSite, ExtSite originExtSite, ObjectNode extData) { - final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode(); - SITE_DATA_ATTR.forEach(attribute -> setAttr(fpdSite, resultData, attribute)); - return originExtSite != null - ? ExtSite.of(originExtSite.getAmp(), resultData.isEmpty() ? null : resultData) - : resultData.isEmpty() ? null : ExtSite.of(null, resultData); + public Device resolveDevice(Device originDevice, ObjectNode fpdDevice) { + return mergeFpd(originDevice, fpdDevice, Device.class); } - public Dooh resolveDooh(Dooh originDooh, ObjectNode fpdDooh) { - if (fpdDooh == null) { - return originDooh; + private T mergeFpd(T original, ObjectNode fpd, Class tClass) { + if (fpd == null || fpd.isNull() || fpd.isMissingNode()) { + return original; } - final Dooh resultDooh = originDooh == null ? Dooh.builder().build() : originDooh; - final ExtDooh resolvedExtDooh = resolveDoohExt(fpdDooh, resultDooh); - return resultDooh.toBuilder() - .name(ObjectUtils.defaultIfNull(getString(fpdDooh, "name"), resultDooh.getName())) - .venuetype(ObjectUtils.defaultIfNull(getStrings(fpdDooh, "venuetype"), resultDooh.getVenuetype())) - .venuetypetax(ObjectUtils.defaultIfNull( - getInteger(fpdDooh, "venuetypetax"), - resultDooh.getVenuetypetax())) - .domain(ObjectUtils.defaultIfNull(getString(fpdDooh, "domain"), resultDooh.getDomain())) - .keywords(ObjectUtils.defaultIfNull(getString(fpdDooh, "keywords"), resultDooh.getKeywords())) - .ext(resolvedExtDooh) - .build(); - } - - private ExtDooh resolveDoohExt(ObjectNode fpdDooh, Dooh originDooh) { - final ExtDooh originExtDooh = originDooh.getExt(); - final ObjectNode resolvedData = - mergeExtData(fpdDooh.path(EXT).path(DATA), originExtDooh != null ? originExtDooh.getData() : null); - return updateDoohExtDataWithFpdAttr(fpdDooh, originExtDooh, resolvedData); - } + final ObjectMapper mapper = jacksonMapper.mapper(); - private ExtDooh updateDoohExtDataWithFpdAttr(ObjectNode fpdDooh, ExtDooh originExtDooh, ObjectNode extData) { - final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode(); - DOOH_DATA_ATTR.forEach(attribute -> setAttr(fpdDooh, resultData, attribute)); - return originExtDooh != null - ? ExtDooh.of(resultData.isEmpty() ? null : resultData) - : resultData.isEmpty() ? null : ExtDooh.of(resultData); + final JsonNode originalAsJsonNode = original != null + ? mapper.valueToTree(original) + : NullNode.getInstance(); + final JsonNode merged = jsonMerger.merge(fpd, originalAsJsonNode); + try { + return mapper.treeToValue(merged, tClass); + } catch (JsonProcessingException e) { + throw new InvalidRequestException("Can't convert merging result class " + tClass.getName()); + } } public ObjectNode resolveImpExt(ObjectNode impExt, ObjectNode targeting) { @@ -277,62 +156,4 @@ private void removeOrReplace(ObjectNode impExt, String field, JsonNode jsonNode) impExt.set(field, jsonNode); } } - - private ObjectNode mergeExtData(JsonNode fpdData, JsonNode originData) { - if (fpdData.isMissingNode() || !fpdData.isObject()) { - return originData != null && originData.isObject() ? ((ObjectNode) originData).deepCopy() : null; - } - - if (originData != null && originData.isObject()) { - return (ObjectNode) jsonMerger.merge(fpdData, originData); - } - return fpdData.isObject() ? (ObjectNode) fpdData : null; - } - - private static void setAttr(ObjectNode source, ObjectNode dest, String fieldName) { - final JsonNode field = source.get(fieldName); - if (field != null) { - dest.set(fieldName, field); - } - } - - private static List getStrings(JsonNode firstItem, String fieldName) { - final JsonNode valueNode = firstItem.get(fieldName); - final ArrayNode arrayNode = valueNode != null && valueNode.isArray() ? (ArrayNode) valueNode : null; - return arrayNode != null && isTextualArray(arrayNode) - ? StreamSupport.stream(arrayNode.spliterator(), false) - .map(JsonNode::asText) - .toList() - : null; - } - - private static boolean isTextualArray(ArrayNode arrayNode) { - return StreamSupport.stream(arrayNode.spliterator(), false).allMatch(JsonNode::isTextual); - } - - private static String getString(ObjectNode firstItem, String fieldName) { - return getValueFromJsonNode(firstItem, fieldName, JsonNode::asText, JsonNode::isTextual); - } - - private static Integer getInteger(ObjectNode firstItem, String fieldName) { - return getValueFromJsonNode(firstItem, fieldName, JsonNode::asInt, JsonNode::isInt); - } - - private List toList(JsonNode node, TypeReference> listTypeReference) { - try { - return jacksonMapper.mapper().convertValue(node, listTypeReference); - } catch (IllegalArgumentException e) { - return null; - } - } - - private static T getValueFromJsonNode(ObjectNode firstItem, String fieldName, - Function nodeConverter, - Predicate isCorrectType) { - final JsonNode valueNode = firstItem.get(fieldName); - return valueNode != null && isCorrectType.test(valueNode) - ? nodeConverter.apply(valueNode) - : null; - } - } 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..609e7481b81 --- /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.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..ca5a565402a --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HookDebugInfoEnricher.java @@ -0,0 +1,246 @@ +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.Activity; +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(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/HooksMetricsService.java b/src/main/java/org/prebid/server/auction/HooksMetricsService.java new file mode 100644 index 00000000000..0b31d28444f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/HooksMetricsService.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction; + +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.ExecutionStatus; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.metric.Metrics; +import org.prebid.server.settings.model.Account; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class HooksMetricsService { + + private final Metrics metrics; + + public HooksMetricsService(Metrics metrics) { + this.metrics = Objects.requireNonNull(metrics); + } + + public AuctionContext updateHooksMetrics(AuctionContext context) { + final EnumMap> stageOutcomes = + context.getHookExecutionContext().getStageOutcomes(); + + final Account account = context.getAccount(); + + stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes)); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + stageOutcomes.values().stream() + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation) + .collect(Collectors.groupingBy( + outcome -> outcome.getHookId().getModuleCode(), + Collectors.summingLong(HookExecutionOutcome::getExecutionTime))) + .forEach((moduleCode, executionTime) -> + metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime)); + } + + return context; + } + + private void updateHooksStageMetrics(Account account, Stage stage, List stageOutcomes) { + stageOutcomes.stream() + .flatMap(stageOutcome -> stageOutcome.getGroups().stream()) + .flatMap(groupOutcome -> groupOutcome.getHooks().stream()) + .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome)); + } + + private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) { + final HookId hookId = hookOutcome.getHookId(); + final ExecutionStatus status = hookOutcome.getStatus(); + final ExecutionAction action = hookOutcome.getAction(); + final String moduleCode = hookId.getModuleCode(); + + metrics.updateHooksMetrics( + moduleCode, + stage, + hookId.getHookImplCode(), + status, + hookOutcome.getExecutionTime(), + action); + + // account might be null if request is rejected by the entrypoint hook + if (account != null) { + metrics.updateAccountHooksMetrics(account, moduleCode, status, action); + } + } +} 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..738622e6a86 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/ImpAdjuster.java @@ -0,0 +1,124 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Imp; +import org.apache.commons.lang3.StringUtils; +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_AE = "ae"; + private static final String EXT_IGS = "igs"; + 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, List debugMessages) { + setAeParams(originalImp.getExt()); + + final JsonNode impExtPrebidImp = bidderParamsFromImpExtPrebidImp(originalImp.getExt()); + if (impExtPrebidImp == null) { + return originalImp; + } + + final JsonNode bidderNode = getBidderNode(bidder, 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 void setAeParams(ObjectNode ext) { + final int extAe = Optional.ofNullable(ext) + .map(extNode -> extNode.get(EXT_AE)) + .filter(JsonNode::isInt) + .map(JsonNode::asInt) + .orElse(-1); + + final boolean extIgsAePresent = Optional.ofNullable(ext) + .map(extNode -> extNode.get(EXT_IGS)) + .map(igsNode -> igsNode.get(EXT_AE)) + .isPresent(); + + if (!extIgsAePresent && (extAe == 0 || extAe == 1)) { + final ObjectNode igsNode = jacksonMapper.mapper().createObjectNode() + .set(EXT_AE, IntNode.valueOf(extAe)); + + ext.set(EXT_IGS, igsNode); + } + } + + 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, JsonNode node) { + final Iterator fieldNames = node.fieldNames(); + while (fieldNames.hasNext()) { + final String fieldName = fieldNames.next(); + if (StringUtils.equalsIgnoreCase(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/ImpMediaTypeResolver.java b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java index 3256ed360e1..964b89b8b3e 100644 --- a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java +++ b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java @@ -31,12 +31,14 @@ private static ImpMediaType resolveBidAdjustmentVideoMediaType(String bidImpId, .orElse(null); if (bidImpVideo == null) { - return null; + return ImpMediaType.video_outstream; } final Integer placement = bidImpVideo.getPlacement(); - return placement == null || Objects.equals(placement, 1) - ? ImpMediaType.video + final Integer plcmt = bidImpVideo.getPlcmt(); + + return Objects.equals(placement, 1) || Objects.equals(plcmt, 1) + ? ImpMediaType.video_instream : ImpMediaType.video_outstream; } } diff --git a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java index 1ffdce56b45..eae090780c5 100644 --- a/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java +++ b/src/main/java/org/prebid/server/auction/ImplicitParametersExtractor.java @@ -1,6 +1,7 @@ package org.prebid.server.auction; import de.malkusch.whoisServerList.publicSuffixList.PublicSuffixList; +import io.vertx.core.MultiMap; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.model.CaseInsensitiveMultiMap; @@ -65,7 +66,7 @@ public List ipFrom(CaseInsensitiveMultiMap headers, String host) { return ipFrom(headers::get, host); } - public List ipFrom(io.vertx.core.MultiMap headers, String host) { + public List ipFrom(MultiMap headers, String host) { return ipFrom(headers::get, host); } diff --git a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java index dd523aff4f6..7aa83960199 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; @@ -93,6 +93,7 @@ private ExtDeviceInt getExtDeviceInt(Device device) { } private static class InterstitialSize { + private static final List INTERSTITIAL_SIZES = new ArrayList<>(); static { @@ -360,8 +361,12 @@ private static InterstitialSize interstitialSize(Integer w, Integer h) { return new InterstitialSize(w, h); } - private static List getNestedSizes(double minWidth, double minHeight, double maxWidth, - double maxHeight, int count) { + private static List getNestedSizes(double minWidth, + double minHeight, + double maxWidth, + double maxHeight, + int count) { + return INTERSTITIAL_SIZES.stream() .filter(size -> isNested(size, minWidth, minHeight, maxWidth, maxHeight)) .limit(count) 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..7f9825b7071 100644 --- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java +++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java @@ -1,32 +1,32 @@ package org.prebid.server.auction; +import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; 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 org.prebid.server.util.StreamUtil; import java.util.ArrayList; -import java.util.Arrays; 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.Optional; import java.util.Set; -import java.util.function.Supplier; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; /** * Service resolves types inconsistency and cast them if possible to ortb2 protocol. @@ -34,42 +34,38 @@ public class OrtbTypesResolver { private static final Logger logger = LoggerFactory.getLogger(OrtbTypesResolver.class); - private static final ConditionalLogger ORTB_TYPES_RESOLVING_LOGGER = + private static final ConditionalLogger ortbTypesResolverLogger = new ConditionalLogger("ortb_resolving_warnings", logger); private static final String USER = "user"; private static final String APP = "app"; private static final String SITE = "site"; + private static final String EXT = "ext"; + private static final String DATA = "data"; + private static final String CONFIG = "config"; + private static final String FPD = "fpd"; + private static final String ORTB2 = "ortb2"; private static final String CONTEXT = "context"; - private static final String BIDREQUEST = "bidrequest"; - private static final String TARGETING = "targeting"; private static final String UNKNOWN_REFERER = "unknown referer"; - private static final String DATA = "data"; - private static final String EXT = "ext"; - private static final Map> FIRST_ARRAY_ELEMENT_STANDARD_FIELDS; - private static final Map> FIRST_ARRAY_ELEMENT_REQUEST_FIELDS; + private static final JsonPointer EXT_PREBID_BIDDER_CONFIG = JsonPointer.valueOf("/ext/prebid/bidderconfig"); + private static final JsonPointer CONFIG_ORTB2 = JsonPointer.valueOf("/config/ortb2"); + private static final JsonPointer APP_BUNDLE = JsonPointer.valueOf("/app/bundle"); + private static final JsonPointer SITE_PAGE = JsonPointer.valueOf("/site/page"); + + private static final Map> FIRST_ARRAY_ELEMENT_FIELDS; private static final Map> COMMA_SEPARATED_ELEMENT_FIELDS; static { - FIRST_ARRAY_ELEMENT_REQUEST_FIELDS = new HashMap<>(); - FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(USER, new HashSet<>(Collections.singleton("gender"))); - FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(APP, new HashSet<>(Arrays.asList("id", "name", "bundle", "storeurl", - "domain"))); - FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(SITE, new HashSet<>(Arrays.asList("id", "name", "domain", "page", - "ref", "search"))); - - FIRST_ARRAY_ELEMENT_STANDARD_FIELDS = new HashMap<>(); - FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(USER, new HashSet<>(Collections.singleton("gender"))); - FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(APP, new HashSet<>(Arrays.asList("name", "bundle", "storeurl", - "domain"))); - FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(SITE, new HashSet<>(Arrays.asList("name", "domain", "page", "ref", - "search"))); - - COMMA_SEPARATED_ELEMENT_FIELDS = new HashMap<>(); - COMMA_SEPARATED_ELEMENT_FIELDS.put(USER, Collections.singleton("keywords")); - COMMA_SEPARATED_ELEMENT_FIELDS.put(APP, Collections.singleton("keywords")); - COMMA_SEPARATED_ELEMENT_FIELDS.put(SITE, Collections.singleton("keywords")); + FIRST_ARRAY_ELEMENT_FIELDS = Map.of( + USER, Collections.singleton("gender"), + APP, Set.of("id", "name", "bundle", "storeurl", "domain"), + SITE, Set.of("id", "name", "domain", "page", "ref", "search")); + + COMMA_SEPARATED_ELEMENT_FIELDS = Map.of( + USER, Collections.singleton("keywords"), + APP, Collections.singleton("keywords"), + SITE, Collections.singleton("keywords")); } private final double logSamplingRate; @@ -83,291 +79,306 @@ public OrtbTypesResolver(double logSamplingRate, JacksonMapper jacksonMapper, Js this.jsonMerger = Objects.requireNonNull(jsonMerger); } - /** - * Resolves fields types inconsistency to ortb2 protocol for {@param bidRequest} for bidRequest level parameters - * and bidderconfig. - * Mutates both parameters, {@param fpdContainerNode} and {@param warnings}. - */ public void normalizeBidRequest(JsonNode bidRequest, List warnings, String referer) { final List resolverWarnings = new ArrayList<>(); - final String rowOriginBidRequest = getOriginalRowContainerNode(bidRequest); - normalizeRequestFpdFields(bidRequest, resolverWarnings); - final JsonNode bidderConfigs = bidRequest.path("ext").path("prebid").path("bidderconfig"); + + normalizeFpdFields(bidRequest, "bidrequest.", resolverWarnings); + + final String source = source(bidRequest); + final JsonNode bidderConfigs = bidRequest.at(EXT_PREBID_BIDDER_CONFIG); if (!bidderConfigs.isMissingNode() && bidderConfigs.isArray()) { for (JsonNode bidderConfig : bidderConfigs) { + mergeFpdFieldsToOrtb2(bidderConfig, source); - mergeFpdFieldsToOrtb2(bidderConfig); - - final JsonNode ortb2Config = bidderConfig.path("config").path("ortb2"); + final JsonNode ortb2Config = bidderConfig.at(CONFIG_ORTB2); if (!ortb2Config.isMissingNode()) { - normalizeStandardFpdFields(ortb2Config, resolverWarnings, "bidrequest.ext.prebid.bidderconfig"); + normalizeFpdFields(ortb2Config, "bidrequest.ext.prebid.bidderconfig.", resolverWarnings); } } } - processWarnings(resolverWarnings, warnings, rowOriginBidRequest, referer, BIDREQUEST); + + processWarnings(resolverWarnings, warnings, referer, "bidrequest", getOriginalRowContainerNode(bidRequest)); } - private String getOriginalRowContainerNode(JsonNode bidRequest) { - try { - return jacksonMapper.mapper().writeValueAsString(bidRequest); - } catch (JsonProcessingException e) { - // should never happen - throw new InvalidRequestException("Failed to decode container node to string"); + private void normalizeFpdFields(JsonNode fpdContainerNode, String prefix, List warnings) { + if (fpdContainerNode != null && fpdContainerNode.isObject()) { + final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode; + updateFpdWithNormalizedNode(fpdContainerObjectNode, USER, warnings, prefix); + updateFpdWithNormalizedNode(fpdContainerObjectNode, APP, warnings, prefix); + updateFpdWithNormalizedNode(fpdContainerObjectNode, SITE, warnings, prefix); } } - /** - * Merges fpd fields into ortb2: - * config.fpd.context -> config.ortb2.site - * config.fpd.user -> config.ortb2.user - */ - private void mergeFpdFieldsToOrtb2(JsonNode bidderConfig) { - final JsonNode config = bidderConfig.path("config"); - final JsonNode configFpd = config.path("fpd"); - - if (configFpd.isMissingNode()) { - return; - } + private static String source(JsonNode bidRequest) { + return Optional.ofNullable(stringAt(bidRequest, APP_BUNDLE)) + .orElseGet(() -> stringAt(bidRequest, SITE_PAGE)); + } - final JsonNode configOrtb = config.path("ortb2"); + private static String stringAt(JsonNode node, JsonPointer path) { + final JsonNode at = node.at(path); + return at.isMissingNode() || at.isNull() || !at.isTextual() + ? null + : at.textValue(); + } - final JsonNode fpdContext = configFpd.get(CONTEXT); - final JsonNode ortbSite = configOrtb.get(SITE); - final JsonNode updatedOrtbSite = ortbSite == null - ? fpdContext - : fpdContext != null ? jsonMerger.merge(fpdContext, ortbSite) : null; + private void updateFpdWithNormalizedNode(ObjectNode containerNode, + String nodeNameToNormalize, + List warnings, + String nodePrefix) { + + updateWithNormalizedNode( + containerNode, + nodeNameToNormalize, + normalizeNode( + containerNode.get(nodeNameToNormalize), + nodeNameToNormalize, + warnings, + nodePrefix)); + } - final JsonNode fpdUser = configFpd.get(USER); - final JsonNode ortbUser = configOrtb.get(USER); - final JsonNode updatedOrtbUser = ortbUser == null - ? fpdUser - : fpdUser != null ? jsonMerger.merge(fpdUser, ortbUser) : null; + private static void updateWithNormalizedNode(ObjectNode containerNode, + String fieldName, + JsonNode normalizedNode) { - if (updatedOrtbUser == null && updatedOrtbSite == null) { - return; + if (normalizedNode == null) { + containerNode.remove(fieldName); + } else { + containerNode.set(fieldName, normalizedNode); } + } - final ObjectNode ortbObjectNode = configOrtb.isMissingNode() - ? jacksonMapper.mapper().createObjectNode() - : (ObjectNode) configOrtb; - - if (updatedOrtbSite != null) { - ortbObjectNode.set(SITE, updatedOrtbSite); + private JsonNode normalizeNode(JsonNode containerNode, String nodeName, List warnings, String nodePrefix) { + if (containerNode == null) { + return null; } + if (!containerNode.isObject()) { + warnings.add("%s%s field ignored. Expected type is object, but was `%s`." + .formatted(nodePrefix, nodeName, containerNode.getNodeType().name())); - if (updatedOrtbUser != null) { - ortbObjectNode.set(USER, updatedOrtbUser); + return null; } - ((ObjectNode) config).set("ortb2", ortbObjectNode); - } + final ObjectNode containerObjectNode = (ObjectNode) containerNode; - /** - * Resolves fields types inconsistency to ortb2 protocol for {@param targeting}. - * Mutates both parameters, {@param targeting} and {@param warnings}. - */ - public void normalizeTargeting(JsonNode targeting, List warnings, String referer) { - final List resolverWarnings = new ArrayList<>(); - final String rowOriginTargeting = getOriginalRowContainerNode(targeting); - normalizeStandardFpdFields(targeting, resolverWarnings, TARGETING); - processWarnings(resolverWarnings, warnings, rowOriginTargeting, referer, TARGETING); - } + normalizeFields( + FIRST_ARRAY_ELEMENT_FIELDS, + nodeName, + containerObjectNode, + (name, node) -> toFirstElementTextNode(name, node, warnings, nodePrefix, nodeName)); + normalizeFields( + COMMA_SEPARATED_ELEMENT_FIELDS, + nodeName, + containerObjectNode, + (name, node) -> toCommaSeparatedTextNode(name, node, warnings, nodePrefix, nodeName)); - /** - * Resolves fields types inconsistency to ortb2 protocol for {@param fpdContainerNode}. - * Mutates both parameters, {@param fpdContainerNode} and {@param warnings}. - */ - private void normalizeStandardFpdFields(JsonNode fpdContainerNode, List warnings, String nodePrefix) { - final String normalizedNodePrefix = nodePrefix.endsWith(".") ? nodePrefix : nodePrefix.concat("."); - if (fpdContainerNode != null && fpdContainerNode.isObject()) { - final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode; - updateWithNormalizedNode(fpdContainerObjectNode, USER, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings); - updateWithNormalizedNode(fpdContainerObjectNode, APP, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings); - updateWithNormalizedNode(fpdContainerObjectNode, SITE, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings); - } - } + normalizeDataExtension(containerObjectNode, warnings, nodePrefix, nodeName); - private void normalizeRequestFpdFields(JsonNode fpdContainerNode, List warnings) { - if (fpdContainerNode != null && fpdContainerNode.isObject()) { - final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode; - final String bidRequestPrefix = BIDREQUEST + "."; - updateWithNormalizedNode(fpdContainerObjectNode, USER, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings); - updateWithNormalizedNode(fpdContainerObjectNode, APP, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings); - updateWithNormalizedNode(fpdContainerObjectNode, SITE, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS, - COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings); - } + return containerNode; } - private void updateWithNormalizedNode(ObjectNode containerNode, String nodeNameToNormalize, - Map> firstArrayElementsFields, - Map> commaSeparatedElementFields, - String nodePrefix, List warnings) { - final JsonNode normalizedNode = normalizeNode(containerNode.get(nodeNameToNormalize), nodeNameToNormalize, - firstArrayElementsFields, commaSeparatedElementFields, nodePrefix, warnings); - if (normalizedNode != null) { - containerNode.set(nodeNameToNormalize, normalizedNode); - } else { - containerNode.remove(nodeNameToNormalize); - } - } + private static void normalizeFields(Map> nodeNameToFields, + String nodeName, + ObjectNode containerObjectNode, + BiFunction fieldNormalizer) { - private JsonNode normalizeNode(JsonNode containerNode, String nodeName, - Map> firstArrayElementsFields, - Map> commaSeparatedElementFields, - String nodePrefix, List warnings) { - if (containerNode != null) { - if (containerNode.isObject()) { - final ObjectNode containerObjectNode = (ObjectNode) containerNode; - - CollectionUtils.emptyIfNull(firstArrayElementsFields.get(nodeName)) - .forEach(fieldName -> updateWithNormalizedField(containerObjectNode, fieldName, - () -> toFirstElementTextNode(containerObjectNode, fieldName, nodeName, nodePrefix, - warnings))); - - CollectionUtils.emptyIfNull(commaSeparatedElementFields.get(nodeName)) - .forEach(fieldName -> updateWithNormalizedField(containerObjectNode, fieldName, - () -> toCommaSeparatedTextNode(containerObjectNode, fieldName, nodeName, nodePrefix, - warnings))); - - normalizeDataExtension(containerObjectNode, nodeName, nodePrefix, warnings); - } else { - warnings.add("%s%s field ignored. Expected type is object, but was `%s`." - .formatted(nodePrefix, nodeName, containerNode.getNodeType().name())); - return null; - } - } - return containerNode; + nodeNameToFields.get(nodeName) + .forEach(fieldName -> updateWithNormalizedNode( + containerObjectNode, + fieldName, + fieldNormalizer.apply(fieldName, containerObjectNode.get(fieldName)))); } - private void updateWithNormalizedField(ObjectNode containerNode, String fieldName, - Supplier normalizationSupplier) { - final JsonNode normalizedField = normalizationSupplier.get(); - if (normalizedField == null) { - containerNode.remove(fieldName); - } else { - containerNode.set(fieldName, normalizedField); - } + private static TextNode toFirstElementTextNode(String fieldName, + JsonNode fieldNode, + List warnings, + String nodePrefix, + String containerName) { + + return toTextNode( + fieldName, + fieldNode, + arrayNode -> arrayNode.get(0).asText(), + warnings, + nodePrefix, + containerName, + "Converted to string by taking first element of array."); } - private JsonNode toFirstElementTextNode(ObjectNode containerNode, - String fieldName, - String containerName, - String nodePrefix, - List warnings) { + private static TextNode toTextNode(String fieldName, + JsonNode fieldNode, + Function mapper, + List warnings, + String nodePrefix, + String containerName, + String action) { - final JsonNode node = containerNode.get(fieldName); - if (node == null || node.isNull() || node.isTextual()) { - return node; + if (fieldNode == null || fieldNode.isNull()) { + return null; } - final boolean isArray = node.isArray(); - final ArrayNode arrayNode = isArray ? (ArrayNode) node : null; - final boolean isTextualArray = arrayNode != null && isTextualArray(arrayNode) && !arrayNode.isEmpty(); + if (fieldNode.isTextual()) { + return (TextNode) fieldNode; + } + + final ArrayNode arrayNode = fieldNode.isArray() ? (ArrayNode) fieldNode : null; + final boolean isTextualArray = arrayNode != null && !arrayNode.isEmpty() && isTextualArray(arrayNode); - if (isTextualArray && !arrayNode.isEmpty()) { + if (isTextualArray) { warnings.add(""" Incorrect type for first party data field %s%s.%s, expected is string, \ - but was an array of strings. Converted to string by taking first element of array.""" - .formatted(nodePrefix, containerName, fieldName)); - return new TextNode(arrayNode.get(0).asText()); + but was an array of strings. %s""" + .formatted(nodePrefix, containerName, fieldName, action)); + + return new TextNode(mapper.apply(arrayNode)); } else { - warnForExpectedStringArrayType(fieldName, containerName, warnings, nodePrefix, node.getNodeType()); + warnForExpectedStringArrayType(warnings, nodePrefix, containerName, fieldName, fieldNode.getNodeType()); return null; } } - private JsonNode toCommaSeparatedTextNode(ObjectNode containerNode, - String fieldName, - String containerName, - String nodePrefix, - List warnings) { + private static boolean isTextualArray(ArrayNode arrayNode) { + return StreamUtil.asStream(arrayNode.iterator()).allMatch(JsonNode::isTextual); + } - final JsonNode node = containerNode.get(fieldName); - if (node == null || node.isNull() || node.isTextual()) { - return node; - } + private static void warnForExpectedStringArrayType(List warnings, + String nodePrefix, + String containerName, + String fieldName, + JsonNodeType nodeType) { - final boolean isArray = node.isArray(); - final ArrayNode arrayNode = isArray ? (ArrayNode) node : null; - final boolean isTextualArray = arrayNode != null && isTextualArray(arrayNode) && !arrayNode.isEmpty(); + warnings.add(""" + Incorrect type for first party data field %s%s.%s, expected strings, \ + but was `%s`. Failed to convert to correct type.""".formatted( + nodePrefix, + containerName, + fieldName, + nodeType == JsonNodeType.ARRAY ? "ARRAY of different types" : nodeType.name())); + } - if (isTextualArray) { - warnings.add(""" - Incorrect type for first party data field %s%s.%s, expected is string, \ - but was an array of strings. Converted to string by separating values with comma.""" - .formatted(nodePrefix, containerName, fieldName)); + private static TextNode toCommaSeparatedTextNode(String fieldName, + JsonNode fieldNode, + List warnings, + String nodePrefix, + String containerName) { - return new TextNode(StreamSupport.stream(arrayNode.spliterator(), false) - .map(jsonNode -> (TextNode) jsonNode) - .map(TextNode::textValue) - .collect(Collectors.joining(","))); - } else { - warnForExpectedStringArrayType(fieldName, containerName, warnings, nodePrefix, node.getNodeType()); - return null; - } + return toTextNode( + fieldName, + fieldNode, + arrayNode -> StreamUtil.asStream(arrayNode.spliterator()) + .map(TextNode.class::cast) + .map(TextNode::textValue) + .collect(Collectors.joining(",")), + warnings, + nodePrefix, + containerName, + "Converted to string by separating values with comma."); } - private void normalizeDataExtension(ObjectNode containerNode, String containerName, String nodePrefix, - List warnings) { + private void normalizeDataExtension(ObjectNode containerNode, + List warnings, + String nodePrefix, + String containerName) { + final JsonNode data = containerNode.get(DATA); if (data == null || !data.isObject()) { return; } + final JsonNode extData = containerNode.path(EXT).path(DATA); final JsonNode ext = containerNode.get(EXT); if (!extData.isNull() && !extData.isMissingNode()) { final JsonNode resolvedExtData = jsonMerger.merge(data, extData); ((ObjectNode) ext).set(DATA, resolvedExtData); } else { - copyDataToExtData(containerNode, containerName, nodePrefix, warnings, data); + copyDataToExtData(containerNode, data, warnings, nodePrefix, containerName); } + containerNode.remove(DATA); } - private void copyDataToExtData(ObjectNode containerNode, String containerName, String nodePrefix, - List warnings, JsonNode data) { + private void copyDataToExtData(ObjectNode containerNode, + JsonNode data, + List warnings, + String nodePrefix, + String containerName) { + final JsonNode ext = containerNode.get(EXT); - if (ext != null && ext.isObject()) { - ((ObjectNode) ext).set(DATA, data); - } else if (ext != null && !ext.isObject()) { + if (ext == null) { + createExtAndCopyData(containerNode, data); + } else if (!ext.isObject()) { warnings.add(""" Incorrect type for first party data field %s%s.%s, \ expected is object, but was %s. Replaced with object""" .formatted(nodePrefix, containerName, EXT, ext.getNodeType())); - containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data)); + createExtAndCopyData(containerNode, data); } else { - containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data)); + ((ObjectNode) ext).set(DATA, data); } } - private void warnForExpectedStringArrayType(String fieldName, String containerName, List warnings, - String nodePrefix, JsonNodeType nodeType) { - warnings.add(""" - Incorrect type for first party data field %s%s.%s, expected strings, \ - but was `%s`. Failed to convert to correct type.""".formatted( - nodePrefix, - containerName, - fieldName, - nodeType == JsonNodeType.ARRAY ? "ARRAY of different types" : nodeType.name())); + private void createExtAndCopyData(ObjectNode containerNode, JsonNode data) { + containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data)); } - private static boolean isTextualArray(ArrayNode arrayNode) { - return StreamSupport.stream(arrayNode.spliterator(), false).allMatch(JsonNode::isTextual); + private void mergeFpdFieldsToOrtb2(JsonNode bidderConfig, String source) { + final JsonNode config = bidderConfig.path(CONFIG); + final JsonNode configFpd = config.path(FPD); + + if (configFpd.isMissingNode()) { + return; + } + + logDeprecatedFpdConfig(source); + + final JsonNode configOrtb = config.path(ORTB2); + final JsonNode updatedOrtbSite = updatedOrtb2Node(configFpd, CONTEXT, configOrtb, SITE); + final JsonNode updatedOrtbUser = updatedOrtb2Node(configFpd, USER, configOrtb, USER); + + if (updatedOrtbUser == null && updatedOrtbSite == null) { + return; + } + + final ObjectNode ortbObjectNode = configOrtb.isMissingNode() + ? jacksonMapper.mapper().createObjectNode() + : (ObjectNode) configOrtb; + + setIfNotNull(ortbObjectNode, SITE, updatedOrtbSite); + setIfNotNull(ortbObjectNode, USER, updatedOrtbUser); + + ((ObjectNode) config).set(ORTB2, ortbObjectNode); + } + + private void logDeprecatedFpdConfig(String source) { + final String messagePart = source != null ? " on " + source : StringUtils.EMPTY; + ortbTypesResolverLogger.warn("Usage of deprecated FPD config path" + messagePart, logSamplingRate); + } + + private JsonNode updatedOrtb2Node(JsonNode configFpd, String fpdField, JsonNode configOrtb, String ortbField) { + final JsonNode fpdNode = configFpd.get(fpdField); + final JsonNode ortbNode = configOrtb.get(ortbField); + return ortbNode == null + ? fpdNode + : fpdNode != null ? jsonMerger.merge(ortbNode, fpdNode) : null; + } + + private static void setIfNotNull(ObjectNode destination, String fieldName, JsonNode data) { + if (data != null) { + destination.set(fieldName, data); + } } - private void processWarnings(List resolverWarning, List warnings, String containerValue, - String referer, String containerName) { - if (CollectionUtils.isNotEmpty(resolverWarning)) { - warnings.addAll(updateWithWarningPrefix(resolverWarning)); - // log only 1% of cases - ORTB_TYPES_RESOLVING_LOGGER.warn( + private void processWarnings(List resolverWarnings, + List warnings, + String referer, + String containerName, + String containerValue) { + + if (CollectionUtils.isNotEmpty(resolverWarnings)) { + warnings.addAll(updateWithWarningPrefix(resolverWarnings)); + + ortbTypesResolverLogger.warn( "WARNINGS: %s. \n Referer = %s and %s = %s".formatted( - String.join("\n", resolverWarning), + String.join("\n", resolverWarnings), StringUtils.isNotBlank(referer) ? referer : UNKNOWN_REFERER, containerName, containerValue), @@ -375,7 +386,22 @@ private void processWarnings(List resolverWarning, List warnings } } - private List updateWithWarningPrefix(List resolverWarning) { + private static List updateWithWarningPrefix(List resolverWarning) { return resolverWarning.stream().map(warning -> "WARNING: " + warning).toList(); } + + private String getOriginalRowContainerNode(JsonNode bidRequest) { + try { + return jacksonMapper.mapper().writeValueAsString(bidRequest); + } catch (JsonProcessingException e) { + // should never happen + throw new InvalidRequestException("Failed to decode container node to string"); + } + } + + public void normalizeTargeting(JsonNode targeting, List warnings, String referer) { + final List resolverWarnings = new ArrayList<>(); + normalizeFpdFields(targeting, "targeting.", resolverWarnings); + processWarnings(resolverWarnings, warnings, referer, "targeting", getOriginalRowContainerNode(targeting)); + } } 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..78e87a44799 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java @@ -0,0 +1,133 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.StoredResponseResult; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.execution.timeout.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 org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class SkippedAuctionService { + + private final StoredResponseProcessor storedResponseProcessor; + + public SkippedAuctionService(StoredResponseProcessor storedResponseProcessor) { + this.storedResponseProcessor = Objects.requireNonNull(storedResponseProcessor); + } + + 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()); + }) + .map(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()); + }) + .map(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 static AuctionContext enrichAuctionContextWithBidResponse(AuctionContext auctionContext, + List seatBids) { + + auctionContext.getDebugWarnings().add("no auction. response defined by storedauctionresponse"); + return auctionContext.with(bidResponse(auctionContext, seatBids)); + } + + private static BidResponse bidResponse(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(); + + return BidResponse.builder() + .id(bidRequest.getId()) + .cur(CollectionUtils.isNotEmpty(cur) ? cur.getFirst() : null) + .seatbid(ListUtils.emptyIfNull(seatBids)) + .ext(extBidResponse) + .build(); + } + + private static Map> extractContextWarnings(AuctionContext auctionContext) { + final List contextWarnings = auctionContext.getDebugWarnings().stream() + .map(message -> ExtBidderError.of(BidderError.Type.generic.getCode(), message)) + .toList(); + + return contextWarnings.isEmpty() + ? Collections.emptyMap() + : Collections.singletonMap("prebid", contextWarnings); + } +} diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java deleted file mode 100644 index 466c1197527..00000000000 --- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java +++ /dev/null @@ -1,385 +0,0 @@ -package org.prebid.server.auction; - -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.BidRequest; -import com.iab.openrtb.request.Imp; -import com.iab.openrtb.response.Bid; -import com.iab.openrtb.response.SeatBid; -import io.vertx.core.Future; -import org.apache.commons.collections4.CollectionUtils; -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.BidderRequest; -import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.auction.model.StoredResponseResult; -import org.prebid.server.auction.model.Tuple2; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.proto.openrtb.ext.request.ExtImp; -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.proto.openrtb.ext.response.BidType; -import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; -import org.prebid.server.settings.ApplicationSettings; -import org.prebid.server.settings.model.StoredResponseDataResult; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -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; - -/** - * Resolves stored response data retrieving and BidderResponse merging processes. - */ -public class StoredResponseProcessor { - - private static final String PREBID_EXT = "prebid"; - private static final String DEFAULT_BID_CURRENCY = "USD"; - private static final String PBS_IMPID_MACRO = "##PBSIMPID##"; - - private static final TypeReference> SEATBID_LIST_TYPE = - new TypeReference<>() { - }; - - private final ApplicationSettings applicationSettings; - private final JacksonMapper mapper; - - public StoredResponseProcessor(ApplicationSettings applicationSettings, - JacksonMapper mapper) { - - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.mapper = Objects.requireNonNull(mapper); - } - - Future getStoredResponseResult(List imps, Timeout timeout) { - final Map impExtPrebids = getImpsExtPrebid(imps); - final Map auctionStoredResponseToImpId = getAuctionStoredResponses(impExtPrebids); - final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, auctionStoredResponseToImpId); - - final Map> impToBidderToStoredBidResponseId = getStoredBidResponses(impExtPrebids, - requiredRequestImps); - - final Set storedIds = new HashSet<>(auctionStoredResponseToImpId.keySet()); - - storedIds.addAll( - impToBidderToStoredBidResponseId.values().stream() - .flatMap(bidderToId -> bidderToId.values().stream()) - .collect(Collectors.toSet())); - - if (storedIds.isEmpty()) { - return Future.succeededFuture(StoredResponseResult.of(imps, Collections.emptyList(), - Collections.emptyMap())); - } - - return applicationSettings.getStoredResponses(storedIds, 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(), - impToBidderToStoredBidResponseId))); - } - - private List excludeStoredAuctionResponseImps(List imps, - Map auctionStoredResponseToImpId) { - - return imps.stream() - .filter(imp -> !auctionStoredResponseToImpId.containsValue(imp.getId())) - .toList(); - } - - 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.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 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) { - 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 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)); - } - - private String extractAuctionStoredResponseId(ExtImpPrebid extImpPrebid) { - final ExtStoredAuctionResponse storedAuctionResponse = extImpPrebid.getStoredAuctionResponse(); - return storedAuctionResponse != null ? storedAuctionResponse.getId() : null; - } - - 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, - 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) { - return storedBidResponse.stream() - .collect(Collectors.toMap(ExtStoredBidResponse::getBidder, ExtStoredBidResponse::getId)); - } - - private List convertToSeatBid(StoredResponseDataResult storedResponseDataResult, - Map auctionStoredResponses) { - final List resolvedSeatBids = new ArrayList<>(); - final Map idToStoredResponses = storedResponseDataResult.getIdToStoredResponses(); - for (final Map.Entry storedIdToImpId : auctionStoredResponses.entrySet()) { - final String id = storedIdToImpId.getKey(); - final String impId = storedIdToImpId.getValue(); - final String rowSeatBid = idToStoredResponses.get(id); - if (rowSeatBid == null) { - throw new InvalidRequestException( - "Failed to fetch stored auction response for impId = %s and storedAuctionResponse id = %s." - .formatted(impId, id)); - } - final List seatBids = parseSeatBid(id, rowSeatBid); - validateStoredSeatBid(seatBids); - resolvedSeatBids.addAll(seatBids.stream() - .map(seatBid -> updateSeatBidBids(seatBid, impId)) - .toList()); - } - return mergeSameBidderSeatBid(resolvedSeatBids); - } - - private List parseSeatBid(String id, String rowSeatBid) { - try { - return mapper.mapper().readValue(rowSeatBid, SEATBID_LIST_TYPE); - } catch (IOException e) { - throw new InvalidRequestException("Can't parse Json for stored response with id " + id); - } - } - - 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())) { - throw new InvalidRequestException("Seat can't be empty in stored response seatBid"); - } - - if (CollectionUtils.isEmpty(seatBid.getBid())) { - throw new InvalidRequestException("There must be at least one bid in stored response seatBid"); - } - } - } - - private List mergeSameBidderSeatBid(List seatBids) { - return seatBids.stream().collect(Collectors.groupingBy(SeatBid::getSeat, Collectors.toList())) - .entrySet().stream() - .map(bidderToSeatBid -> makeMergedSeatBid(bidderToSeatBid.getKey(), bidderToSeatBid.getValue())) - .toList(); - } - - private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { - return SeatBid.builder() - .bid(storedSeatBids.stream().map(SeatBid::getBid).flatMap(List::stream).toList()) - .seat(seat) - .ext(storedSeatBids.stream().map(SeatBid::getExt).filter(Objects::nonNull).findFirst().orElse(null)) - .build(); - } - - private Map> mapStoredBidResponseIdsToValues( - Map idToStoredResponses, - Map> impToBidderToStoredBidResponseId) { - - return impToBidderToStoredBidResponseId.entrySet().stream() - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> entry.getValue().entrySet().stream() - .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue())) - .collect(Collectors.toMap( - Map.Entry::getKey, - bidderToId -> idToStoredResponses.get(bidderToId.getValue()), - (first, second) -> second, - CaseInsensitiveMap::new)))); - } - - private AuctionParticipation updateBidderResponse(AuctionParticipation auctionParticipation, - SeatBid storedSeatBid, - Map impIdToBidType) { - if (auctionParticipation != null) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid bidderSeatBid = bidderResponse.getSeatBid(); - final BidderSeatBid updatedSeatBid = storedSeatBid == null - ? bidderSeatBid - : makeBidderSeatBid(bidderSeatBid, storedSeatBid, impIdToBidType); - final BidderResponse updatedBidderResponse = BidderResponse.of(bidderResponse.getBidder(), - updatedSeatBid, bidderResponse.getResponseTime()); - return auctionParticipation.with(updatedBidderResponse); - } else { - final String bidder = storedSeatBid != null ? storedSeatBid.getSeat() : null; - final BidderSeatBid updatedSeatBid = makeBidderSeatBid(null, storedSeatBid, impIdToBidType); - final BidderResponse updatedBidderResponse = BidderResponse.of(bidder, updatedSeatBid, 0); - return AuctionParticipation.builder() - .bidder(bidder) - .bidderResponse(updatedBidderResponse) - .build(); - } - } - - 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) - : DEFAULT_BID_CURRENCY; - final List bidderBids = seatBid != null - ? seatBid.getBid().stream() - .map(bid -> makeBidderBid(bid, bidCurrency, impIdToBidType)) - .collect(Collectors.toCollection(ArrayList::new)) - : new ArrayList<>(); - if (nonNullBidderSeatBid) { - bidderBids.addAll(bidderSeatBid.getBids()); - } - return nonNullBidderSeatBid - ? bidderSeatBid.with(bidderBids) - : BidderSeatBid.of(bidderBids); - } - - private BidderBid makeBidderBid(Bid bid, String bidCurrency, Map impIdToBidType) { - return BidderBid.of(bid, getBidType(bid.getExt(), impIdToBidType.get(bid.getImpid())), bidCurrency); - } - - private BidType getBidType(ObjectNode bidExt, BidType bidType) { - final ObjectNode bidExtPrebid = bidExt != null ? (ObjectNode) bidExt.get(PREBID_EXT) : null; - final ExtBidPrebid extBidPrebid = bidExtPrebid != null ? parseExtBidPrebid(bidExtPrebid) : null; - return extBidPrebid != null ? extBidPrebid.getType() : bidType; - } - - private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExtPrebid) { - try { - return mapper.mapper().treeToValue(bidExtPrebid, ExtBidPrebid.class); - } catch (JsonProcessingException e) { - throw new PreBidException("Error decoding stored response bid.ext.prebid"); - } - } - - 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; - } -} 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..16af4e15d71 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -2,7 +2,10 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.BidderError; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.util.ArrayList; @@ -11,7 +14,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; /** * Used throughout Prebid to create targeting keys as keys which can be used in an ad server like DFP. @@ -32,10 +34,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". */ @@ -80,14 +78,12 @@ public class TargetingKeywordsCreator { */ private static final String FORMAT_KEY = "_format"; - private static final String DEFAULT_CPM = "0.0"; - private final PriceGranularity priceGranularity; private final boolean includeWinners; 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, @@ -156,7 +152,9 @@ Map makeFor(Bid bid, String cacheId, String format, String vastCacheId, - String categoryDuration) { + String categoryDuration, + Account account, + Map> bidWarnings) { final Map keywords = makeFor( bidder, @@ -168,16 +166,17 @@ Map makeFor(Bid bid, vastCacheId, categoryDuration, format, - bid.getDealid()); + bid.getDealid(), + account); if (resolver == null) { - return truncateKeys(keywords); + return truncateKeys(keywords, bidWarnings); } final Map augmentedKeywords = new HashMap<>(keywords); augmentedKeywords.putAll(resolver.resolve(bid, bidder)); - return truncateKeys(augmentedKeywords); + return truncateKeys(augmentedKeywords, bidWarnings); } /** @@ -192,7 +191,8 @@ private Map makeFor(String bidder, String vastCacheId, String categoryDuration, String format, - String dealId) { + String dealId, + Account account) { final boolean includeDealBid = alwaysIncludeDeals && StringUtils.isNotEmpty(dealId); final KeywordMap keywordMap = new KeywordMap( @@ -202,7 +202,10 @@ private Map makeFor(String bidder, includeBidderKeys || includeDealBid, Collections.emptySet()); - final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : DEFAULT_CPM; + final String roundedCpm = isPriceGranularityValid() + ? CpmRange.fromCpm(price, priceGranularity, account) + : CpmRange.DEFAULT_CPM; + keywordMap.put(this.keyPrefix + PB_KEY, roundedCpm); keywordMap.put(this.keyPrefix + BIDDER_KEY, bidder); @@ -230,8 +233,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); @@ -258,12 +261,33 @@ private static String sizeFrom(Integer width, Integer height) { : null; } - private Map truncateKeys(Map keyValues) { - return truncateAttrChars > 0 - ? keyValues.entrySet().stream() - .collect(Collectors - .toMap(keyValue -> truncateKey(keyValue.getKey()), Map.Entry::getValue, (key1, key2) -> key1)) - : keyValues; + private Map truncateKeys(Map keyValues, + Map> bidWarnings) { + + if (truncateAttrChars <= 0) { + return keyValues; + } + + final Map keys = new HashMap<>(); + final List truncatedKeys = new ArrayList<>(); + for (Map.Entry entry : keyValues.entrySet()) { + final String key = entry.getKey(); + final String truncatedKey = truncateKey(key); + keys.putIfAbsent(truncatedKey, entry.getValue()); + + if (truncatedKey.length() != key.length()) { + truncatedKeys.add(key); + } + } + + if (!truncatedKeys.isEmpty()) { + final String errorMessage = "The following keys have been truncated: %s" + .formatted(String.join(", ", truncatedKeys)); + bidWarnings.computeIfAbsent("targeting", ignored -> new ArrayList<>()) + .add(ExtBidderError.of(BidderError.Type.bad_input.getCode(), errorMessage)); + } + + return keys; } private String truncateKey(String key) { @@ -275,7 +299,7 @@ private String truncateKey(String key) { /** * Helper for targeting keywords. *

- * Brings a convenient way for creating keywords regarding to bidder and winning bid flag. + * Brings a convenient way for creating keywords regarding bidder and winning bid flag. */ private static class KeywordMap { diff --git a/src/main/java/org/prebid/server/auction/TimeoutResolver.java b/src/main/java/org/prebid/server/auction/TimeoutResolver.java index 4b9d2411eb1..44ae2984aa1 100644 --- a/src/main/java/org/prebid/server/auction/TimeoutResolver.java +++ b/src/main/java/org/prebid/server/auction/TimeoutResolver.java @@ -32,16 +32,16 @@ public long limitToMax(Long timeout) { : Math.min(timeout, maxTimeout); } - public long adjustForBidder(long timeout, int adjustFactor, long spentTime) { - return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime); + public long adjustForBidder(long timeout, int adjustFactor, long spentTime, long bidderTmaxDeductionMs) { + return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime, bidderTmaxDeductionMs); } public long adjustForRequest(long timeout, long spentTime) { - return adjustWithFactor(timeout, 1.0, spentTime); + return adjustWithFactor(timeout, 1.0, spentTime, 0L); } - private long adjustWithFactor(long timeout, double adjustFactor, long spentTime) { - return limitToMin((long) (timeout * adjustFactor) - spentTime - upstreamResponseTime); + private long adjustWithFactor(long timeout, double adjustFactor, long spentTime, long deductionTime) { + return limitToMin((long) (timeout * adjustFactor) - spentTime - deductionTime - upstreamResponseTime); } private long limitToMin(long timeout) { diff --git a/src/main/java/org/prebid/server/auction/UidUpdater.java b/src/main/java/org/prebid/server/auction/UidUpdater.java index 5c2390a6db3..014c0d93898 100644 --- a/src/main/java/org/prebid/server/auction/UidUpdater.java +++ b/src/main/java/org/prebid/server/auction/UidUpdater.java @@ -3,6 +3,7 @@ import com.iab.openrtb.request.User; import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cookie.UidsCookie; diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java index 91ded451731..f3beeb57e6c 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; @@ -26,9 +24,11 @@ import org.prebid.server.auction.model.Tuple2; import org.prebid.server.auction.model.WithPodErrors; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.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( @@ -129,9 +129,9 @@ private static BidRequest readBidRequest(String defaultBidRequestPath, : null; } - private StoredDataResult updateMetrics(StoredDataResult storedDataResult, - Set requestIds, - Set impIds) { + private StoredDataResult updateMetrics(StoredDataResult storedDataResult, + Set requestIds, + Set impIds) { requestIds.forEach( id -> metrics.updateStoredRequestMetric(storedDataResult.getStoredIdToRequest().containsKey(id))); @@ -142,12 +142,12 @@ private StoredDataResult updateMetrics(StoredDataResult storedDataResult, return storedDataResult; } - private WithPodErrors toBidRequestWithPodErrors(StoredDataResult storedResult, + private WithPodErrors toBidRequestWithPodErrors(StoredDataResult storedResult, BidRequestVideo videoRequest, 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(); @@ -161,7 +161,7 @@ private WithPodErrors toBidRequestWithPodErrors(StoredDataResult sto private BidRequestVideo mergeBidRequest(BidRequestVideo originalRequest, String storedRequestId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); if (enforceStoredRequest && StringUtils.isBlank(storedRequest)) { 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/BidAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java deleted file mode 100644 index fa0013d683f..00000000000 --- a/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.prebid.server.auction.adjustment; - -import org.apache.commons.collections4.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; - -import java.math.BigDecimal; -import java.util.EnumMap; -import java.util.Map; -import java.util.Optional; - -public class BidAdjustmentFactorResolver { - - public BigDecimal resolve(ImpMediaType impMediaType, - ExtRequestBidAdjustmentFactors adjustmentFactors, - String bidder) { - - final EnumMap> adjustmentFactorsByMediaTypes = - adjustmentFactors.getMediatypes(); - - final BigDecimal effectiveBidderAdjustmentFactor = Optional.ofNullable(adjustmentFactors.getAdjustments()) - .map(factors -> factors.get(bidder)) - .orElse(BigDecimal.ONE); - - if (MapUtils.isEmpty(adjustmentFactorsByMediaTypes)) { - return effectiveBidderAdjustmentFactor; - } - - return Optional.ofNullable(impMediaType) - .map(adjustmentFactorsByMediaTypes::get) - .flatMap(factors -> factors.entrySet().stream() - .filter(entry -> StringUtils.equalsIgnoreCase(entry.getKey(), bidder)) - .map(Map.Entry::getValue) - .findFirst()) - .orElse(effectiveBidderAdjustmentFactor); - } -} diff --git a/src/main/java/org/prebid/server/auction/aliases/AlternateBidder.java b/src/main/java/org/prebid/server/auction/aliases/AlternateBidder.java new file mode 100644 index 00000000000..aab0494cd4f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/aliases/AlternateBidder.java @@ -0,0 +1,10 @@ +package org.prebid.server.auction.aliases; + +import java.util.Set; + +public interface AlternateBidder { + + Boolean getEnabled(); + + Set getAllowedBidderCodes(); +} diff --git a/src/main/java/org/prebid/server/auction/aliases/AlternateBidderCodesConfig.java b/src/main/java/org/prebid/server/auction/aliases/AlternateBidderCodesConfig.java new file mode 100644 index 00000000000..0e91f87fe54 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/aliases/AlternateBidderCodesConfig.java @@ -0,0 +1,10 @@ +package org.prebid.server.auction.aliases; + +import java.util.Map; + +public interface AlternateBidderCodesConfig { + + Boolean getEnabled(); + + Map getBidders(); +} diff --git a/src/main/java/org/prebid/server/auction/aliases/BidderAliases.java b/src/main/java/org/prebid/server/auction/aliases/BidderAliases.java new file mode 100644 index 00000000000..aade755a9ec --- /dev/null +++ b/src/main/java/org/prebid/server/auction/aliases/BidderAliases.java @@ -0,0 +1,131 @@ +package org.prebid.server.auction.aliases; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.collections4.map.CaseInsensitiveMap; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.BidderCatalog; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * Represents aliases configured for bidders - configuration might come in OpenRTB request but not limited to it. + */ +public class BidderAliases { + + private static final String WILDCARD = "*"; + + private final Map aliasToBidder; + + private final Map aliasToVendorId; + + private final Map> bidderToAllowedBidderCodes; + + private final BidderCatalog bidderCatalog; + + private BidderAliases(Map aliasToBidder, + Map aliasToVendorId, + Map> bidderToAllowedBidderCodes, + BidderCatalog bidderCatalog) { + + this.aliasToBidder = new CaseInsensitiveMap<>(MapUtils.emptyIfNull(aliasToBidder)); + this.aliasToVendorId = new CaseInsensitiveMap<>(MapUtils.emptyIfNull(aliasToVendorId)); + this.bidderToAllowedBidderCodes = MapUtils.emptyIfNull(bidderToAllowedBidderCodes); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + public static BidderAliases of(Map aliasToBidder, + Map aliasToVendorId, + BidderCatalog bidderCatalog) { + + return new BidderAliases(aliasToBidder, aliasToVendorId, null, bidderCatalog); + } + + public static BidderAliases of(Map aliasToBidder, + Map aliasToVendorId, + BidderCatalog bidderCatalog, + AlternateBidderCodesConfig alternateBidderCodes) { + + return new BidderAliases( + aliasToBidder, + aliasToVendorId, + resolveAlternateBidderCodes(alternateBidderCodes), + bidderCatalog); + } + + public boolean isAliasDefined(String alias) { + return aliasToBidder.containsKey(alias); + } + + public String resolveBidder(String aliasOrBidder) { + return bidderCatalog.isValidName(aliasOrBidder) + ? aliasOrBidder + : aliasToBidder.getOrDefault(aliasOrBidder, aliasOrBidder); + } + + public boolean isSame(String bidder1, String bidder2) { + return StringUtils.equalsIgnoreCase(resolveBidder(bidder1), resolveBidder(bidder2)); + } + + public Integer resolveAliasVendorId(String alias) { + final Integer vendorId = resolveAliasVendorIdViaCatalog(alias); + return vendorId == null ? aliasToVendorId.get(alias) : vendorId; + } + + private Integer resolveAliasVendorIdViaCatalog(String alias) { + final String bidderName = resolveBidder(alias); + return bidderCatalog.isActive(bidderName) ? bidderCatalog.vendorIdByName(bidderName) : null; + } + + public boolean isAllowedAlternateBidderCode(String bidder, String alternateBidderCode) { + final Set allowedBidderCodes = ObjectUtils.firstNonNull( + bidderToAllowedBidderCodes.get(bidder), + bidderToAllowedBidderCodes.get(resolveBidder(bidder)), + Collections.emptySet()); + return allowedBidderCodes.contains(WILDCARD) || allowedBidderCodes.contains(alternateBidderCode); + } + + public boolean isKnownAlternateBidderCode(String alternateBidderCode) { + return bidderToAllowedBidderCodes.values().stream() + .anyMatch(knownBidderCodes -> + knownBidderCodes.contains(WILDCARD) || knownBidderCodes.contains(alternateBidderCode)); + } + + private static Map> resolveAlternateBidderCodes( + AlternateBidderCodesConfig alternateBidderCodes) { + + return Optional.ofNullable(alternateBidderCodes) + .filter(config -> BooleanUtils.isTrue(config.getEnabled())) + .map(AlternateBidderCodesConfig::getBidders) + .map(Map::entrySet) + .stream() + .flatMap(Collection::stream) + .filter(entry -> BooleanUtils.isTrue(entry.getValue().getEnabled())) + .map(entry -> Map.entry(entry.getKey(), allowedBidderCodes(entry.getValue()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (first, second) -> second, + CaseInsensitiveMap::new)); + } + + private static Set allowedBidderCodes(AlternateBidder alternateBidder) { + final Set allowedBidderCodes = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + final Set alternateCodes = alternateBidder.getAllowedBidderCodes(); + + if (alternateCodes == null) { + allowedBidderCodes.add(WILDCARD); + } else { + allowedBidderCodes.addAll(alternateCodes); + } + return allowedBidderCodes; + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java new file mode 100644 index 00000000000..e470fcbbdd0 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCleaner.java @@ -0,0 +1,37 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +public class BidderRequestCleaner implements BidderRequestPostProcessor { + + @Override + public Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final BidRequest bidRequest = bidderRequest.getBidRequest(); + final ExtRequest ext = bidRequest.getExt(); + final ExtRequestPrebid extPrebid = ext != null ? ext.getPrebid() : null; + final ObjectNode bidderControls = extPrebid != null ? extPrebid.getBiddercontrols() : null; + + if (bidderControls == null) { + return resultOf(bidderRequest); + } + + final ExtRequest cleanedExt = ExtRequest.of(extPrebid.toBuilder().biddercontrols(null).build()); + cleanedExt.addProperties(ext.getProperties()); + + return resultOf(bidderRequest.with(bidRequest.toBuilder().ext(cleanedExt).build())); + } + + private static Future resultOf(BidderRequest bidderRequest) { + return Future.succeededFuture(BidderRequestPostProcessingResult.withValue(bidderRequest)); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java new file mode 100644 index 00000000000..a131d8a6a50 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestCurrencyBlocker.java @@ -0,0 +1,54 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.bidder.model.BidderError; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +public class BidderRequestCurrencyBlocker implements BidderRequestPostProcessor { + + private final BidderCatalog bidderCatalog; + + public BidderRequestCurrencyBlocker(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + if (isAcceptableCurrency(bidderRequest.getBidRequest(), aliases.resolveBidder(bidderRequest.getBidder()))) { + return Future.succeededFuture(BidderRequestPostProcessingResult.withValue(bidderRequest)); + } + + return Future.failedFuture(new BidderRequestRejectedException( + BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY, + Collections.singletonList( + BidderError.generic("No match between the configured currencies and bidRequest.cur")))); + } + + private boolean isAcceptableCurrency(BidRequest bidRequest, String originalBidderName) { + final List requestCurrencies = bidRequest.getCur(); + final Set bidAcceptableCurrencies = + Optional.ofNullable(bidderCatalog.bidderInfoByName(originalBidderName)) + .map(BidderInfo::getCurrencyAccepted) + .orElse(null); + + return CollectionUtils.isEmpty(requestCurrencies) + || CollectionUtils.isEmpty(bidAcceptableCurrencies) + || requestCurrencies.stream().anyMatch(bidAcceptableCurrencies::contains); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java new file mode 100644 index 00000000000..b3946a11081 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestMediaFilter.java @@ -0,0 +1,139 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +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.Native; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.spring.config.bidder.model.MediaType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidderRequestMediaFilter implements BidderRequestPostProcessor { + + private static final EnumSet NONE_OF_MEDIA_TYPES = EnumSet.noneOf(MediaType.class); + + private final BidderCatalog bidderCatalog; + + public BidderRequestMediaFilter(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final String resolvedBidderName = aliases.resolveBidder(bidderRequest.getBidder()); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + final Set supportedMediaTypes = extractSupportedMediaTypes(bidRequest, resolvedBidderName); + if (supportedMediaTypes.isEmpty()) { + return rejected(Collections.singletonList( + BidderError.badInput("Bidder does not support any media types."))); + } + + final List errors = new ArrayList<>(); + final BidRequest modifiedBidRequest = processBidRequest(bidRequest, supportedMediaTypes, errors); + final BidderRequest modifiedBidderRequest = modifiedBidRequest != null + ? bidderRequest.with(modifiedBidRequest) + : null; + + return modifiedBidderRequest != null + ? Future.succeededFuture(BidderRequestPostProcessingResult.of(modifiedBidderRequest, errors)) + : rejected(errors); + } + + private static Future rejected(List errors) { + return Future.failedFuture( + new BidderRequestRejectedException(BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, errors)); + } + + private Set extractSupportedMediaTypes(BidRequest bidRequest, String bidderName) { + final BidderInfo.CapabilitiesInfo capabilitiesInfo = bidderCatalog + .bidderInfoByName(bidderName) + .getCapabilities(); + + final BidderInfo.PlatformInfo site = bidRequest.getSite() != null ? capabilitiesInfo.getSite() : null; + final BidderInfo.PlatformInfo app = bidRequest.getApp() != null ? capabilitiesInfo.getApp() : null; + final BidderInfo.PlatformInfo dooh = bidRequest.getDooh() != null ? capabilitiesInfo.getDooh() : null; + + return Stream.of(site, app, dooh) + .filter(Objects::nonNull) + .findFirst() + .map(BidderInfo.PlatformInfo::getMediaTypes) + .filter(mediaTypes -> !mediaTypes.isEmpty()) + .map(EnumSet::copyOf) + .orElse(NONE_OF_MEDIA_TYPES); + } + + private static BidRequest processBidRequest(BidRequest bidRequest, + Set supportedMediaTypes, + List errors) { + + final List modifiedImps = bidRequest.getImp().stream() + .map(imp -> processImp(imp, supportedMediaTypes, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (modifiedImps.isEmpty()) { + errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); + return null; + } + + return bidRequest.toBuilder().imp(modifiedImps).build(); + } + + private static Imp processImp(Imp imp, Set supportedMediaTypes, List errors) { + final Set impMediaTypes = getMediaTypes(imp); + if (supportedMediaTypes.containsAll(impMediaTypes)) { + return imp; + } + + final Banner banner = supportedMediaTypes.contains(MediaType.BANNER) ? imp.getBanner() : null; + final Video video = supportedMediaTypes.contains(MediaType.VIDEO) ? imp.getVideo() : null; + final Audio audio = supportedMediaTypes.contains(MediaType.AUDIO) ? imp.getAudio() : null; + final Native xNative = supportedMediaTypes.contains(MediaType.NATIVE) ? imp.getXNative() : null; + + if (ObjectUtils.allNull(banner, video, audio, xNative)) { + errors.add(BidderError.badInput(""" + Imp %s does not have a supported media type \ + and has been removed from the request for this bidder.""".formatted(imp.getId()))); + + return null; + } + + return imp.toBuilder() + .banner(banner) + .video(video) + .audio(audio) + .xNative(xNative) + .build(); + } + + private static Set getMediaTypes(Imp imp) { + return Stream.of( + imp.getBanner() != null ? MediaType.BANNER : null, + imp.getVideo() != null ? MediaType.VIDEO : null, + imp.getAudio() != null ? MediaType.AUDIO : null, + imp.getXNative() != null ? MediaType.NATIVE : null) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(MediaType.class))); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessingResult.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessingResult.java new file mode 100644 index 00000000000..f38d9a1a423 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessingResult.java @@ -0,0 +1,20 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import lombok.Value; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.model.BidderError; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class BidderRequestPostProcessingResult { + + BidderRequest value; + + List errors; + + public static BidderRequestPostProcessingResult withValue(BidderRequest bidderRequest) { + return BidderRequestPostProcessingResult.of(bidderRequest, Collections.emptyList()); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java new file mode 100644 index 00000000000..e2fc97f225f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPostProcessor.java @@ -0,0 +1,13 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; + +public interface BidderRequestPostProcessor { + + Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext); +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java new file mode 100644 index 00000000000..fb685298a4a --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestPreferredMediaProcessor.java @@ -0,0 +1,155 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import com.fasterxml.jackson.databind.JsonNode; +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.Native; +import com.iab.openrtb.request.Video; +import io.vertx.core.Future; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.bidder.BidderCatalog; +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.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.spring.config.bidder.model.MediaType; +import org.prebid.server.util.StreamUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class BidderRequestPreferredMediaProcessor implements BidderRequestPostProcessor { + + private static final String PREF_MTYPE_FIELD = "prefmtype"; + + private final BidderCatalog bidderCatalog; + + public BidderRequestPreferredMediaProcessor(BidderCatalog bidderCatalog) { + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + } + + @Override + public Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + final String bidderName = bidderRequest.getBidder(); + final BidRequest bidRequest = bidderRequest.getBidRequest(); + + final String resolvedBidderName = aliases.resolveBidder(bidderName); + if (isMultiFormatSupported(resolvedBidderName)) { + return noAction(bidderRequest); + } + + final Optional preferredMediaType = preferredMediaType(bidRequest, bidderName) + .or(() -> preferredMediaType(auctionContext.getAccount(), resolvedBidderName)); + if (preferredMediaType.isEmpty()) { + return noAction(bidderRequest); + } + + final List errors = new ArrayList<>(); + final BidRequest modifiedBidRequest = processBidRequest(bidRequest, preferredMediaType.get(), errors); + final BidderRequest modifiedBidderRequest = modifiedBidRequest != null + ? bidderRequest.with(modifiedBidRequest) + : null; + + return modifiedBidderRequest != null + ? Future.succeededFuture(BidderRequestPostProcessingResult.of(modifiedBidderRequest, errors)) + : Future.failedFuture(new BidderRequestRejectedException( + BidRejectionReason.REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE, errors)); + } + + private static Future noAction(BidderRequest bidderRequest) { + return Future.succeededFuture(BidderRequestPostProcessingResult.withValue(bidderRequest)); + } + + private boolean isMultiFormatSupported(String bidder) { + return bidderCatalog.bidderInfoByName(bidder).getOrtb().isMultiFormatSupported(); + } + + private static Optional preferredMediaType(BidRequest bidRequest, String bidderName) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBiddercontrols) + .flatMap(bidders -> getBidder(bidderName, bidders)) + .map(bidder -> bidder.get(PREF_MTYPE_FIELD)) + .filter(JsonNode::isTextual) + .map(JsonNode::textValue) + .map(MediaType::of); + } + + private static Optional preferredMediaType(Account account, String bidderName) { + return Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getPreferredMediaTypes) + .map(preferredMediaTypes -> preferredMediaTypes.get(bidderName)); + } + + private static Optional getBidder(String bidderName, JsonNode biddersNode) { + return StreamUtil.asStream(biddersNode.fieldNames()) + .filter(fieldName -> StringUtils.equalsIgnoreCase(bidderName, fieldName)) + .map(biddersNode::get) + .findAny(); + } + + private static BidRequest processBidRequest(BidRequest bidRequest, + MediaType preferredMediaType, + List errors) { + + final List modifiedImps = bidRequest.getImp().stream() + .map(imp -> processImp(imp, preferredMediaType, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + if (modifiedImps.isEmpty()) { + errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); + return null; + } + + return bidRequest.toBuilder().imp(modifiedImps).build(); + } + + private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { + if (!isMultiFormat(imp)) { + return imp; + } + + final Banner banner = preferredMediaType == MediaType.BANNER ? imp.getBanner() : null; + final Video video = preferredMediaType == MediaType.VIDEO ? imp.getVideo() : null; + final Audio audio = preferredMediaType == MediaType.AUDIO ? imp.getAudio() : null; + final Native xNative = preferredMediaType == MediaType.NATIVE ? imp.getXNative() : null; + + if (ObjectUtils.allNull(banner, video, audio, xNative)) { + errors.add(BidderError.badInput(""" + Imp %s does not have a media type after filtering \ + and has been removed from the request for this bidder.""".formatted(imp.getId()))); + + return null; + } + + return imp.toBuilder() + .banner(banner) + .video(video) + .audio(audio) + .xNative(xNative) + .build(); + } + + private static boolean isMultiFormat(Imp imp) { + int count = 0; + return (imp.getBanner() != null && ++count > 1) + || (imp.getVideo() != null && ++count > 1) + || (imp.getAudio() != null && ++count > 1) + || (imp.getXNative() != null && ++count > 1); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java new file mode 100644 index 00000000000..6beeaa5069f --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/BidderRequestRejectedException.java @@ -0,0 +1,20 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import lombok.Getter; +import org.prebid.server.auction.model.BidRejectionReason; +import org.prebid.server.bidder.model.BidderError; + +import java.util.List; +import java.util.Objects; + +@Getter +public class BidderRequestRejectedException extends RuntimeException { + + private final BidRejectionReason rejectionReason; + private final List errors; + + public BidderRequestRejectedException(BidRejectionReason bidRejectionReason, List errors) { + this.rejectionReason = Objects.requireNonNull(bidRejectionReason); + this.errors = Objects.requireNonNull(errors); + } +} diff --git a/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java new file mode 100644 index 00000000000..b483c2ad63e --- /dev/null +++ b/src/main/java/org/prebid/server/auction/bidderrequestpostprocessor/CompositeBidderRequestPostProcessor.java @@ -0,0 +1,46 @@ +package org.prebid.server.auction.bidderrequestpostprocessor; + +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.util.ListUtil; + +import java.util.List; +import java.util.Objects; + +public class CompositeBidderRequestPostProcessor implements BidderRequestPostProcessor { + + private final List bidderRequestPostProcessors; + + public CompositeBidderRequestPostProcessor(List bidderRequestPostProcessors) { + this.bidderRequestPostProcessors = Objects.requireNonNull(bidderRequestPostProcessors); + } + + @Override + public Future process(BidderRequest bidderRequest, + BidderAliases aliases, + AuctionContext auctionContext) { + + Future result = initialResult(bidderRequest); + for (BidderRequestPostProcessor bidderRequestPostProcessor : bidderRequestPostProcessors) { + result = result.compose(previous -> + bidderRequestPostProcessor.process(previous.getValue(), aliases, auctionContext) + .map(current -> mergeErrors(previous, current))); + } + + return result; + } + + private static Future initialResult(BidderRequest bidderRequest) { + return Future.succeededFuture(BidderRequestPostProcessingResult.withValue(bidderRequest)); + } + + private static BidderRequestPostProcessingResult mergeErrors(BidderRequestPostProcessingResult previous, + BidderRequestPostProcessingResult current) { + + return BidderRequestPostProcessingResult.of( + current.getValue(), + ListUtil.union(previous.getErrors(), current.getErrors())); + } +} 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..480f8c9dd49 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -28,7 +28,7 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.JacksonMapper; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; import org.prebid.server.proto.openrtb.ext.request.ExtDealTier; @@ -42,6 +42,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; import org.prebid.server.util.ObjectUtil; import java.math.BigDecimal; @@ -83,6 +84,7 @@ public BasicCategoryMappingService(ApplicationSettings applicationSettings, Jack @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { final ExtRequestTargeting targeting = targeting(bidRequest); @@ -110,9 +112,21 @@ public Future createCategoryMapping(List final List rejectedBids = new ArrayList<>(); return makeBidderToBidCategory( - bidderResponses, withCategory, translateCategories, primaryAdServer, publisher, rejectedBids, timeout) + bidderResponses, + withCategory, + translateCategories, + primaryAdServer, + publisher, + rejectedBids, + timeout) .map(categoryBidContexts -> resolveBidsCategoriesDurations( - bidderResponses, categoryBidContexts, bidRequest, targeting, withCategory, rejectedBids)); + bidderResponses, + categoryBidContexts, + account, + bidRequest, + targeting, + withCategory, + rejectedBids)); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { @@ -150,7 +164,7 @@ private Future> makeBidderToBidCategory(List> categoryBidContextsPromise = Promise.promise(); - final CompositeFuture compositeFuture = CompositeFuture.join(bidderResponses.stream() + final CompositeFuture compositeFuture = Future.join(bidderResponses.stream() .flatMap(bidderResponse -> makeFetchCategoryFutures( bidderResponse, primaryAdServer, publisher, timeout, withCategory, translateCategories)) .collect(Collectors.toList())); @@ -205,7 +219,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")); @@ -326,6 +340,7 @@ private static void collectCategoryFetchResults(CompositeFuture compositeFuture, */ private CategoryMappingResult resolveBidsCategoriesDurations(List bidderResponses, List categoryBidContexts, + Account account, BidRequest bidRequest, ExtRequestTargeting targeting, boolean withCategory, @@ -342,8 +357,15 @@ private CategoryMappingResult resolveBidsCategoriesDurations(List> uniqueCatKeysToCategoryBids = categoryBidContexts.stream() - .map(categoryBidContext -> enrichCategoryBidContext(categoryBidContext, durations, priceGranularity, - withCategory, appendBidderNames, impIdToBiddersDealTear, rejectedBids)) + .map(categoryBidContext -> enrichCategoryBidContext( + categoryBidContext, + account, + durations, + priceGranularity, + withCategory, + appendBidderNames, + impIdToBiddersDealTear, + rejectedBids)) .filter(Objects::nonNull) .collect(Collectors.groupingBy(CategoryBidContext::getCategoryUniqueKey, Collectors.mapping(Function.identity(), Collectors.toSet()))); @@ -504,6 +526,7 @@ private static boolean isNotRejected(String bidId, String bidder, List durations, PriceGranularity priceGranularity, boolean withCategory, @@ -522,7 +545,7 @@ private CategoryBidContext enrichCategoryBidContext(CategoryBidContext categoryB return null; } - final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity); + final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity, account); final String rowPrice = CpmRange.format(price, priceGranularity.getPrecision()); final String category = categoryBidContext.getCategory(); final String categoryUniqueKey = createCategoryUniqueKey(withCategory, category, rowPrice, duration); @@ -556,7 +579,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)); @@ -729,14 +752,6 @@ private static RejectedBid of(String bidId, String bidder, String errorMessage) @Builder(toBuilder = true) private static class CategoryBidContext { - public static CategoryBidContext of(BidderBid bidderBid, String bidder, String category) { - return CategoryBidContext.builder() - .bidderBid(bidderBid) - .bidder(bidder) - .category(category) - .build(); - } - BidderBid bidderBid; String bidder; @@ -750,6 +765,14 @@ public static CategoryBidContext of(BidderBid bidderBid, String bidder, String c BigDecimal price; boolean satisfiedPriority; + + public static CategoryBidContext of(BidderBid bidderBid, String bidder, String category) { + return CategoryBidContext.builder() + .bidderBid(bidderBid) + .bidder(bidder) + .category(category) + .build(); + } } @Value(staticConstructor = "of") diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java index 088a9604b9d..e99f05dd815 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java @@ -4,7 +4,8 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -12,5 +13,6 @@ public interface CategoryMappingService { Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java index f6161fa6f90..4bf54857955 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java @@ -4,7 +4,8 @@ import io.vertx.core.Future; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -13,6 +14,7 @@ public class NoOpCategoryMappingService implements CategoryMappingService { @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { return Future.succeededFuture(CategoryMappingResult.of(bidderResponses)); diff --git a/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java new file mode 100644 index 00000000000..52685241178 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/externalortb/ProfilesProcessor.java @@ -0,0 +1,308 @@ +package org.prebid.server.auction.externalortb; + +import com.fasterxml.jackson.core.JsonProcessingException; +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 io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.exception.InvalidProfileException; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +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.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.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountProfilesConfig; +import org.prebid.server.settings.model.Profile; +import org.prebid.server.settings.model.StoredDataResult; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +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.stream.Collectors; + +public class ProfilesProcessor { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ProfilesProcessor.class)); + + private final int maxProfiles; + private final long defaultTimeoutMillis; + private final boolean failOnUnknown; + private final double logSamplingRate; + private final ApplicationSettings applicationSettings; + private final TimeoutFactory timeoutFactory; + private final Metrics metrics; + private final JacksonMapper mapper; + private final JsonMerger jsonMerger; + + public ProfilesProcessor(int maxProfiles, + long defaultTimeoutMillis, + boolean failOnUnknown, + double logSamplingRate, + ApplicationSettings applicationSettings, + TimeoutFactory timeoutFactory, + Metrics metrics, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + this.maxProfiles = maxProfiles; + this.defaultTimeoutMillis = defaultTimeoutMillis; + this.failOnUnknown = failOnUnknown; + this.logSamplingRate = logSamplingRate; + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + this.metrics = Objects.requireNonNull(metrics); + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public Future process(AuctionContext auctionContext, BidRequest bidRequest) { + final String accountId = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getId) + .orElse(StringUtils.EMPTY); + + final AllProfilesIds profilesIds = profilesIds(bidRequest, auctionContext, accountId); + if (profilesIds.isEmpty()) { + return Future.succeededFuture(bidRequest); + } + + final boolean failOnUnknown = isFailOnUnknown(auctionContext.getAccount()); + + return fetchProfiles(accountId, profilesIds, timeoutMillis(bidRequest)) + .compose(profiles -> emitMetrics(accountId, profiles, auctionContext, failOnUnknown)) + .map(profiles -> mergeResults( + applyRequestProfiles( + profilesIds.request(), + profiles.getStoredIdToRequest(), + bidRequest, + failOnUnknown), + applyImpsProfiles( + profilesIds.imps(), + profiles.getStoredIdToImp(), + bidRequest.getImp(), + failOnUnknown))) + .recover(error -> Future.failedFuture( + new InvalidRequestException("Error during processing profiles: " + error.getMessage()))); + } + + private AllProfilesIds profilesIds(BidRequest bidRequest, AuctionContext auctionContext, String accountId) { + final AllProfilesIds initialProfilesIds = new AllProfilesIds( + requestProfilesIds(bidRequest), + Optional.ofNullable(bidRequest) + .map(BidRequest::getImp) + .orElse(Collections.emptyList()) + .stream() + .map(this::impProfilesIds) + .toList()); + + final AllProfilesIds profilesIds = truncate( + initialProfilesIds, + Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getProfiles) + .map(AccountProfilesConfig::getLimit) + .orElse(maxProfiles)); + + if (auctionContext.getDebugContext().isDebugEnabled() && !profilesIds.equals(initialProfilesIds)) { + auctionContext.getDebugWarnings().add("Profiles exceeded the limit."); + metrics.updateAccountProfileMetric(accountId, MetricName.limit_exceeded); + } + + return profilesIds; + } + + private static List requestProfilesIds(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getProfiles) + .orElse(Collections.emptyList()); + } + + private List impProfilesIds(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("prebid")) + .map(this::parseImpExt) + .map(ExtImpPrebid::getProfiles) + .orElse(Collections.emptyList()); + } + + private ExtImpPrebid parseImpExt(JsonNode jsonNode) { + try { + return mapper.mapper().treeToValue(jsonNode, ExtImpPrebid.class); + } catch (JsonProcessingException e) { + throw new InvalidRequestException(e.getMessage()); + } + } + + private static AllProfilesIds truncate(AllProfilesIds profilesIds, int maxProfiles) { + final List requestProfiles = profilesIds.request(); + final int impProfilesLimit = Math.max(0, maxProfiles - requestProfiles.size()); + + return new AllProfilesIds( + truncate(requestProfiles, maxProfiles), + profilesIds.imps().stream() + .map(impProfiles -> truncate(impProfiles, impProfilesLimit)) + .toList()); + } + + private static List truncate(List list, int maxSize) { + return list.size() > maxSize ? list.subList(0, maxSize) : list; + } + + private long timeoutMillis(BidRequest bidRequest) { + final Long tmax = bidRequest.getTmax(); + return tmax != null && tmax > 0 ? tmax : defaultTimeoutMillis; + } + + private boolean isFailOnUnknown(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getProfiles) + .map(AccountProfilesConfig::getFailOnUnknown) + .orElse(failOnUnknown); + } + + private Future> fetchProfiles(String accountId, + AllProfilesIds allProfilesIds, + long timeoutMillis) { + + final Set requestProfilesIds = new HashSet<>(allProfilesIds.request()); + final Set impProfilesIds = allProfilesIds.imps().stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + final Timeout timeout = timeoutFactory.create(timeoutMillis); + + return applicationSettings.getProfiles(accountId, requestProfilesIds, impProfilesIds, timeout); + } + + private Future> emitMetrics(String accountId, + StoredDataResult fetchResult, + AuctionContext auctionContext, + boolean failOnUnknown) { + + final List errors = fetchResult.getErrors(); + if (!errors.isEmpty()) { + metrics.updateProfileMetric(MetricName.missing); + + if (auctionContext.getDebugContext().isDebugEnabled()) { + metrics.updateAccountProfileMetric(accountId, MetricName.missing); + auctionContext.getDebugWarnings().addAll(errors); + } + + if (failOnUnknown) { + return Future.failedFuture(new InvalidProfileException(errors)); + } + } + + return Future.succeededFuture(fetchResult); + } + + private BidRequest applyRequestProfiles(List profilesIds, + Map idToRequestProfile, + BidRequest bidRequest, + boolean failOnUnknown) { + + return !idToRequestProfile.isEmpty() + ? applyProfiles(profilesIds, idToRequestProfile, bidRequest, failOnUnknown) + : bidRequest; + } + + private T applyProfiles(List profilesIds, + Map idToProfile, + T original, + boolean failOnUnknown) { + + if (profilesIds.isEmpty()) { + return original; + } + + ObjectNode result = mapper.mapper().valueToTree(original); + for (String profileId : profilesIds) { + try { + final Profile profile = idToProfile.get(profileId); + result = profile != null ? mergeProfile(result, profile) : result; + } catch (InvalidRequestException e) { + final String message = "Can't merge with profile %s: %s".formatted(profileId, e.getMessage()); + + metrics.updateProfileMetric(MetricName.invalid); + conditionalLogger.error(message, logSamplingRate); + if (failOnUnknown) { + throw new InvalidProfileException(message); + } + } + } + + try { + return mapper.mapper().treeToValue(result, (Class) original.getClass()); + } catch (JsonProcessingException e) { + throw new InvalidProfileException(e.getMessage()); + } + } + + private ObjectNode mergeProfile(ObjectNode original, Profile profile) { + return switch (profile.getMergePrecedence()) { + case REQUEST -> merge(original, profile.getBody()); + case PROFILE -> merge(profile.getBody(), original); + }; + } + + private ObjectNode merge(JsonNode takePrecedence, JsonNode other) { + if (!takePrecedence.isObject() || !other.isObject()) { + throw new InvalidRequestException("One of the merge arguments is not an object."); + } + + return (ObjectNode) jsonMerger.merge(takePrecedence, other); + } + + private List applyImpsProfiles(List> profilesIds, + Map idToImpProfile, + List imps, + boolean failOnUnknown) { + + if (idToImpProfile.isEmpty()) { + return imps; + } + + final List updatedImps = new ArrayList<>(imps); + for (int i = 0; i < profilesIds.size(); i++) { + updatedImps.set(i, applyProfiles( + profilesIds.get(i), + idToImpProfile, + imps.get(i), + failOnUnknown)); + } + + return Collections.unmodifiableList(updatedImps); + } + + private static BidRequest mergeResults(BidRequest bidRequest, List imps) { + return bidRequest.toBuilder().imp(imps).build(); + } + + private record AllProfilesIds(List request, List> imps) { + + public boolean isEmpty() { + return request.isEmpty() && imps.stream().allMatch(List::isEmpty); + } + } +} diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java similarity index 94% rename from src/main/java/org/prebid/server/auction/StoredRequestProcessor.java rename to src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java index f982870c049..a725c02b228 100644 --- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java +++ b/src/main/java/org/prebid/server/auction/externalortb/StoredRequestProcessor.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction; +package org.prebid.server.auction.externalortb; import com.fasterxml.jackson.core.JsonProcessingException; import com.iab.openrtb.request.BidRequest; @@ -12,8 +12,8 @@ import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.InvalidStoredImpException; import org.prebid.server.exception.InvalidStoredRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.identity.IdGenerator; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; @@ -104,7 +104,7 @@ private Future processAuctionStoredRequest(String accountId return Future.succeededFuture(AuctionStoredResult.of(false, bidRequest)); } - final Future storedDataFuture = + final Future> storedDataFuture = applicationSettings.getStoredData(accountId, requestIds, impIds, timeout(bidRequest)) .onSuccess(storedDataResult -> updateStoredResultMetrics(storedDataResult, requestIds, impIds)); @@ -121,7 +121,7 @@ public Future processAmpRequest(String accountId, String ampRequestI } private Future processAmpStoredRequest(String accountId, String ampRequestId, BidRequest bidRequest) { - final Future ampStoredDataFuture = applicationSettings.getAmpStoredData( + final Future> ampStoredDataFuture = applicationSettings.getAmpStoredData( accountId, Collections.singleton(ampRequestId), Collections.emptySet(), timeout(bidRequest)) .onSuccess(storedDataResult -> updateStoredResultMetrics( storedDataResult, Collections.singleton(ampRequestId), Collections.emptySet())); @@ -130,10 +130,10 @@ private Future processAmpStoredRequest(String accountId, String ampR .map(this::generateBidRequestId); } - Future videoStoredDataResult(String accountId, - List imps, - List errors, - Timeout timeout) { + public Future videoStoredDataResult(String accountId, + List imps, + List errors, + Timeout timeout) { return videoStoredDataResultInternal(accountId, imps, errors, timeout) .onFailure(cause -> updateInvalidStoredResultMetrics(accountId, cause)) @@ -172,7 +172,7 @@ private static Future stripToInvalidRequestException(Throwable cause) { "Stored request processing failed: " + cause.getMessage())); } - private void updateStoredResultMetrics(StoredDataResult storedDataResult, + private void updateStoredResultMetrics(StoredDataResult storedDataResult, Set requestIds, Set impIds) { @@ -192,7 +192,7 @@ private static BidRequest readBidRequest(String defaultBidRequestPath, : null; } - private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, + private VideoStoredDataResult makeVideoStoredDataResult(StoredDataResult storedDataResult, Map storedIdToImpId, List errors) { @@ -232,7 +232,7 @@ private Video parseVideoFromImp(String storedJson) { return null; } - private Future storedRequestsToBidRequest(Future storedDataFuture, + private Future storedRequestsToBidRequest(Future> storedDataFuture, BidRequest bidRequest, String storedBidRequestId, Map impsToStoredRequestId) { @@ -253,7 +253,7 @@ private Future storedRequestsToBidRequest(Future s private BidRequest mergeBidRequestAndImps(BidRequest bidRequest, String storedRequestId, Map impToStoredId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final BidRequest mergedWithStoredRequest = mergeBidRequest(bidRequest, storedRequestId, storedDataResult); @@ -272,7 +272,7 @@ private BidRequest mergeDefaultRequest(BidRequest bidRequest) { */ private BidRequest mergeBidRequest(BidRequest originalRequest, String storedRequestId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { final String storedRequest = storedDataResult.getStoredIdToRequest().get(storedRequestId); return StringUtils.isNotBlank(storedRequestId) @@ -286,7 +286,7 @@ private BidRequest mergeBidRequest(BidRequest originalRequest, */ private BidRequest mergeImps(BidRequest bidRequest, Map impToStoredId, - StoredDataResult storedDataResult) { + StoredDataResult storedDataResult) { if (impToStoredId.isEmpty()) { return bidRequest; diff --git a/src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java new file mode 100644 index 00000000000..b44939a38e6 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/externalortb/StoredResponseProcessor.java @@ -0,0 +1,484 @@ +package org.prebid.server.auction.externalortb; + +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.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +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; +import org.prebid.server.auction.model.Tuple2; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtImp; +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.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.StoredResponseDataResult; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +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.function.Function; +import java.util.stream.Collectors; + +/** + * Resolves stored response data retrieving and BidderResponse merging processes. + */ +public class StoredResponseProcessor { + + private static final String PREBID_EXT = "prebid"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String PBS_IMPID_MACRO = "##PBSIMPID##"; + + private static final TypeReference> SEATBID_LIST_TYPE = + new TypeReference<>() { + }; + + private final ApplicationSettings applicationSettings; + private final JacksonMapper mapper; + + public StoredResponseProcessor(ApplicationSettings applicationSettings, + JacksonMapper mapper) { + + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.mapper = Objects.requireNonNull(mapper); + } + + public Future getStoredResponseResult(List imps, Timeout timeout) { + final Map impExtPrebids = getImpsExtPrebid(imps); + final Map impIdsToStoredResponses = getAuctionStoredResponses(impExtPrebids); + final List requiredRequestImps = excludeStoredAuctionResponseImps(imps, impIdsToStoredResponses); + + final Map> impToBidderToStoredBidResponseId = + getStoredBidResponses(impExtPrebids, requiredRequestImps); + + final Set storedResponses = new HashSet<>(impIdsToStoredResponses.values()); + + impToBidderToStoredBidResponseId.values() + .forEach(bidderToStoredResponse -> storedResponses.addAll(bidderToStoredResponse.values())); + + if (storedResponses.isEmpty()) { + return Future.succeededFuture( + StoredResponseResult.of(imps, Collections.emptyList(), Collections.emptyMap())); + } + + 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, impIdsToStoredResponses), + mapStoredBidResponseIdsToValues( + storedResponseDataResult.getIdToStoredResponses(), + impToBidderToStoredBidResponseId))); + } + + public 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())); + } + + private Map getImpsExtPrebid(List imps) { + return imps.stream() + .collect(Collectors.toMap(Imp::getId, imp -> getExtImp(imp.getExt(), imp.getId()).getPrebid())); + } + + 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 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 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); + } + + private List excludeStoredAuctionResponseImps(List imps, + Map impIdToStoredResponse) { + + return imps.stream() + .filter(imp -> !impIdToStoredResponse.containsKey(imp.getId())) + .toList(); + } + + 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(); + } + + return extImpPrebids.entrySet().stream() + .filter(impIdToExtPrebid -> + CollectionUtils.isNotEmpty(impIdToExtPrebid.getValue().getStoredBidResponse())) + .collect(Collectors.toMap( + Map.Entry::getKey, + impIdToStoredResponses -> + resolveStoredBidResponse(impIdToStoredResponses.getValue().getStoredBidResponse()))); + } + + private Map resolveStoredBidResponse( + List storedBidResponse) { + + return storedBidResponse.stream() + .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 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 (Map.Entry storedIdToImpId : idToStoredResponses.entrySet()) { + final String id = storedIdToImpId.getKey(); + final String rowSeatBid = storedIdToImpId.getValue(); + if (rowSeatBid == null) { + throw new InvalidRequestException( + "Failed to fetch stored auction response for storedAuctionResponse id = %s.".formatted(id)); + } + final List seatBids = parseSeatBid(id, rowSeatBid); + validateStoredSeatBid(seatBids); + 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); + } catch (IOException e) { + throw new InvalidRequestException("Can't parse Json for stored response with id " + id); + } + } + + private void validateStoredSeatBid(List seatBids) { + for (final SeatBid seatBid : seatBids) { + if (StringUtils.isEmpty(seatBid.getSeat())) { + throw new InvalidRequestException("Seat can't be empty in stored response seatBid"); + } + + if (CollectionUtils.isEmpty(seatBid.getBid())) { + throw new InvalidRequestException("There must be at least one bid in stored response seatBid"); + } + } + } + + 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() + .map(bidderToSeatBid -> makeMergedSeatBid(bidderToSeatBid.getKey(), bidderToSeatBid.getValue())) + .toList(); + } + + private SeatBid makeMergedSeatBid(String seat, List storedSeatBids) { + return SeatBid.builder() + .bid(storedSeatBids.stream().map(SeatBid::getBid).flatMap(List::stream).toList()) + .seat(seat) + .ext(storedSeatBids.stream().map(SeatBid::getExt).filter(Objects::nonNull).findFirst().orElse(null)) + .build(); + } + + private Map> mapStoredBidResponseIdsToValues( + Map idToStoredResponses, + Map> impToBidderToStoredBidResponseId) { + + return impToBidderToStoredBidResponseId.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().entrySet().stream() + .filter(bidderToId -> idToStoredResponses.containsKey(bidderToId.getValue().id())) + .collect(Collectors.toMap( + Map.Entry::getKey, + 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(); + } + + public 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; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid bidderSeatBid = bidderResponse.getSeatBid(); + final BidderSeatBid updatedSeatBid = storedSeatBid == null + ? bidderSeatBid + : makeBidderSeatBid(bidderSeatBid, storedSeatBid, impIdToBidType); + final BidderResponse updatedBidderResponse = BidderResponse.of(bidderResponse.getBidder(), + updatedSeatBid, bidderResponse.getResponseTime()); + return auctionParticipation.with(updatedBidderResponse); + } else { + final String bidder = storedSeatBid != null ? storedSeatBid.getSeat() : null; + final BidderSeatBid updatedSeatBid = makeBidderSeatBid(null, storedSeatBid, impIdToBidType); + final BidderResponse updatedBidderResponse = BidderResponse.of(bidder, updatedSeatBid, 0); + return AuctionParticipation.builder() + .bidder(bidder) + .bidderResponse(updatedBidderResponse) + .build(); + } + } + + 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) + : DEFAULT_BID_CURRENCY; + final List bidderBids = seatBid != null + ? seatBid.getBid().stream() + .map(bid -> makeBidderBid(bid, bidCurrency, seatBid.getSeat(), impIdToBidType)) + .collect(Collectors.toCollection(ArrayList::new)) + : new ArrayList<>(); + if (nonNullBidderSeatBid) { + bidderBids.addAll(bidderSeatBid.getBids()); + } + return nonNullBidderSeatBid + ? bidderSeatBid.with(bidderBids) + : BidderSeatBid.of(bidderBids); + } + + private BidderBid makeBidderBid(Bid bid, String bidCurrency, String seat, Map impIdToBidType) { + return BidderBid.of(bid, getBidType(bid.getExt(), impIdToBidType.get(bid.getImpid())), seat, bidCurrency); + } + + private BidType getBidType(ObjectNode bidExt, BidType bidType) { + final ObjectNode bidExtPrebid = bidExt != null ? (ObjectNode) bidExt.get(PREBID_EXT) : null; + final ExtBidPrebid extBidPrebid = bidExtPrebid != null ? parseExtBidPrebid(bidExtPrebid) : null; + return extBidPrebid != null ? extBidPrebid.getType() : bidType; + } + + private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExtPrebid) { + try { + return mapper.mapper().treeToValue(bidExtPrebid, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + throw new PreBidException("Error decoding stored response bid.ext.prebid"); + } + } + + 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 { + } + } +} 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/BidderMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java deleted file mode 100644 index 0171ff90b68..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessor.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.apache.commons.collections4.SetUtils; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.bidder.BidderCatalog; -import org.prebid.server.bidder.BidderInfo; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.settings.model.Account; -import org.prebid.server.spring.config.bidder.model.MediaType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * {@link BidderMediaTypeProcessor} is an implementation of {@link MediaTypeProcessor} that - * can be used to remove media types from {@link Imp} unsupported by specific bidder. - */ -public class BidderMediaTypeProcessor implements MediaTypeProcessor { - - private static final EnumSet NONE_OF_MEDIA_TYPES = EnumSet.noneOf(MediaType.class); - - private final BidderCatalog bidderCatalog; - - public BidderMediaTypeProcessor(BidderCatalog bidderCatalog) { - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - } - - @Override - public MediaTypeProcessingResult process(BidRequest bidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - final String resolvedBidderName = aliases.resolveBidder(bidderName); - final Set supportedMediaTypes = extractSupportedMediaTypes(bidRequest, resolvedBidderName); - if (supportedMediaTypes.isEmpty()) { - return MediaTypeProcessingResult.rejected(Collections.singletonList( - BidderError.badInput("Bidder does not support any media types."))); - } - - final List errors = new ArrayList<>(); - final BidRequest modifiedBidRequest = processBidRequest(bidRequest, supportedMediaTypes, errors); - - return modifiedBidRequest != null - ? MediaTypeProcessingResult.succeeded(modifiedBidRequest, errors) - : MediaTypeProcessingResult.rejected(errors); - } - - private Set extractSupportedMediaTypes(BidRequest bidRequest, String bidderName) { - final BidderInfo.CapabilitiesInfo capabilitiesInfo = bidderCatalog.bidderInfoByName(bidderName) - .getCapabilities(); - - final Supplier fetchSupportedMediaTypes; - if (bidRequest.getSite() != null) { - fetchSupportedMediaTypes = capabilitiesInfo::getSite; - } else if (bidRequest.getApp() != null) { - fetchSupportedMediaTypes = capabilitiesInfo::getApp; - } else { - fetchSupportedMediaTypes = capabilitiesInfo::getDooh; - } - - return Optional.ofNullable(fetchSupportedMediaTypes.get()) - .map(BidderInfo.PlatformInfo::getMediaTypes) - .filter(mediaTypes -> !mediaTypes.isEmpty()) - .map(EnumSet::copyOf) - .orElse(NONE_OF_MEDIA_TYPES); - } - - private BidRequest processBidRequest(BidRequest bidRequest, - Set supportedMediaTypes, - List errors) { - - final List modifiedImps = bidRequest.getImp().stream() - .map(imp -> processImp(imp, supportedMediaTypes, errors)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - if (modifiedImps.isEmpty()) { - errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); - return null; - } - - return bidRequest.toBuilder().imp(modifiedImps).build(); - } - - private static Imp processImp(Imp imp, Set supportedMediaTypes, List errors) { - final Set impMediaTypes = getMediaTypes(imp); - final Set unsupportedMediaTypes = SetUtils.difference(impMediaTypes, supportedMediaTypes); - - if (unsupportedMediaTypes.isEmpty()) { - return imp; - } - - if (impMediaTypes.equals(unsupportedMediaTypes)) { - errors.add(BidderError.badInput("Imp " + imp.getId() + " does not have a supported media type " - + "and has been removed from the request for this bidder.")); - - return null; - } - - final Imp.ImpBuilder impBuilder = imp.toBuilder(); - unsupportedMediaTypes.forEach(unsupportedMediaType -> removeMediaType(impBuilder, unsupportedMediaType)); - - return impBuilder.build(); - } - - private static Set getMediaTypes(Imp imp) { - return Stream.of( - imp.getBanner() != null ? MediaType.BANNER : null, - imp.getVideo() != null ? MediaType.VIDEO : null, - imp.getAudio() != null ? MediaType.AUDIO : null, - imp.getXNative() != null ? MediaType.NATIVE : null) - .filter(Objects::nonNull) - .collect(Collectors.toCollection(() -> EnumSet.noneOf(MediaType.class))); - } - - private static void removeMediaType(Imp.ImpBuilder impBuilder, MediaType mediaType) { - switch (mediaType) { - case BANNER -> impBuilder.banner(null); - case VIDEO -> impBuilder.video(null); - case AUDIO -> impBuilder.audio(null); - case NATIVE -> impBuilder.xNative(null); - } - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java deleted file mode 100644 index c51d35eb33f..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/CompositeMediaTypeProcessor.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.bidder.model.BidderError; -import org.prebid.server.settings.model.Account; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class CompositeMediaTypeProcessor implements MediaTypeProcessor { - - private final List mediaTypeProcessors; - - public CompositeMediaTypeProcessor(List mediaTypeProcessors) { - this.mediaTypeProcessors = Objects.requireNonNull(mediaTypeProcessors); - } - - @Override - public MediaTypeProcessingResult process(BidRequest originalBidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - BidRequest bidRequest = originalBidRequest; - final List errors = new ArrayList<>(); - - for (MediaTypeProcessor mediaTypeProcessor : mediaTypeProcessors) { - final MediaTypeProcessingResult result = mediaTypeProcessor.process( - bidRequest, - bidderName, - aliases, - account); - - bidRequest = result.getBidRequest(); - errors.addAll(result.getErrors()); - - if (result.isRejected()) { - return MediaTypeProcessingResult.rejected(errors); - } - } - - return MediaTypeProcessingResult.succeeded(bidRequest, errors); - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java deleted file mode 100644 index b9a6780d2e8..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessingResult.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import org.prebid.server.bidder.model.BidderError; - -import java.util.List; - -@Value(staticConstructor = "of") -public class MediaTypeProcessingResult { - - BidRequest bidRequest; - - List errors; - - boolean rejected; - - public static MediaTypeProcessingResult succeeded(BidRequest bidRequest, List errors) { - return MediaTypeProcessingResult.of(bidRequest, errors, false); - } - - public static MediaTypeProcessingResult rejected(List errors) { - return MediaTypeProcessingResult.of(null, errors, true); - } -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java deleted file mode 100644 index 89e7f07589b..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MediaTypeProcessor.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.settings.model.Account; - -public interface MediaTypeProcessor { - - MediaTypeProcessingResult process(BidRequest bidRequest, String bidderName, BidderAliases aliases, Account account); -} diff --git a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java b/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java deleted file mode 100644 index f4e89ea2f76..00000000000 --- a/src/main/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessor.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.prebid.server.auction.mediatypeprocessor; - -import com.fasterxml.jackson.databind.JsonNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; -import org.prebid.server.auction.BidderAliases; -import org.prebid.server.bidder.BidderCatalog; -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.settings.model.Account; -import org.prebid.server.settings.model.AccountAuctionConfig; -import org.prebid.server.spring.config.bidder.model.MediaType; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class MultiFormatMediaTypeProcessor implements MediaTypeProcessor { - - private static final String PREF_MTYPE_FIELD = "prefmtype"; - - private final BidderCatalog bidderCatalog; - - public MultiFormatMediaTypeProcessor(BidderCatalog bidderCatalog) { - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - } - - @Override - public MediaTypeProcessingResult process(BidRequest bidRequest, - String bidderName, - BidderAliases aliases, - Account account) { - final String resolvedBidderName = aliases.resolveBidder(bidderName); - //todo: ext.prebid.biddercontrols clean-up should NOT be here - // Suggestion: keep biddercontrols in the Auction Context - // and clean it up on the extraction auction participants step - final BidRequest.BidRequestBuilder bidRequestBuilder = Optional.ofNullable(bidRequest.getExt()) - .map(ExtRequest::getPrebid) - .map(prebid -> prebid.toBuilder().biddercontrols(null).build()) - .map(prebid -> { - final ExtRequest extRequest = ExtRequest.of(prebid); - extRequest.addProperties(bidRequest.getExt().getProperties()); - return extRequest; - }) - .map(extRequest -> bidRequest.toBuilder().ext(extRequest)) - .orElse(bidRequest.toBuilder()); - if (isMultiFormatSupported(resolvedBidderName)) { - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.build(), Collections.emptyList()); - } - - final MediaType preferredMediaType = preferredMediaType(bidRequest, account, bidderName, resolvedBidderName); - if (preferredMediaType == null) { - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.build(), Collections.emptyList()); - } - - final List errors = new ArrayList<>(); - final List updatedImps = bidRequest.getImp().stream() - .map(imp -> processImp(imp, preferredMediaType, errors)) - .filter(Objects::nonNull) - .toList(); - - if (updatedImps.isEmpty()) { - errors.add(BidderError.badInput("Bid request contains 0 impressions after filtering.")); - return MediaTypeProcessingResult.rejected(errors); - } - - return MediaTypeProcessingResult.succeeded(bidRequestBuilder.imp(updatedImps).build(), errors); - } - - private boolean isMultiFormatSupported(String bidder) { - return bidderCatalog.bidderInfoByName(bidder).getOrtb().isMultiFormatSupported(); - } - - 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(bidder -> bidder.get(PREF_MTYPE_FIELD)) - .filter(JsonNode::isTextual) - .map(JsonNode::textValue) - .map(MediaType::of) - .or(() -> Optional.ofNullable(account.getAuction()) - .map(AccountAuctionConfig::getPreferredMediaTypes) - .map(preferredMediaTypes -> preferredMediaTypes.get(resolvedBidderName))) - .orElse(null); - } - - private static Imp processImp(Imp imp, MediaType preferredMediaType, List errors) { - if (!isMultiFormat(imp)) { - return imp; - } - - final Imp updatedImp = switch (preferredMediaType) { - case BANNER -> imp.getBanner() != null - ? imp.toBuilder().video(null).audio(null).xNative(null).build() - : null; - case VIDEO -> imp.getVideo() != null - ? imp.toBuilder().banner(null).audio(null).xNative(null).build() - : null; - case AUDIO -> imp.getAudio() != null - ? imp.toBuilder().banner(null).video(null).xNative(null).build() - : null; - case NATIVE -> imp.getXNative() != null - ? imp.toBuilder().banner(null).video(null).audio(null).build() - : null; - }; - - if (updatedImp == null) { - errors.add(BidderError.badInput("Imp " + imp.getId() + " does not have a media type after filtering " - + "and has been removed from the request for this bidder.")); - } - return updatedImp; - } - - private static boolean isMultiFormat(Imp imp) { - int count = 0; - return (imp.getBanner() != null && ++count > 1) - || (imp.getVideo() != null && ++count > 1) - || (imp.getAudio() != null && ++count > 1) - || (imp.getXNative() != null && ++count > 1); - } -} 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..224b6344f34 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -21,20 +21,24 @@ public class BidInfo { String bidder; + String seat; + BidType bidType; CacheInfo cacheInfo; - String lineItemId; - - String lineItemSource; - TargetingInfo targetingInfo; String category; Boolean satisfiedPriority; + Integer ttl; + + Integer vastTtl; + + Integer rank; + 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/BidRejection.java b/src/main/java/org/prebid/server/auction/model/BidRejection.java new file mode 100644 index 00000000000..17f8924447b --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/BidRejection.java @@ -0,0 +1,31 @@ +package org.prebid.server.auction.model; + +import lombok.Value; +import org.prebid.server.bidder.model.BidderBid; + +@Value(staticConstructor = "of") +public class BidRejection implements Rejection { + + BidderBid bid; + + BidRejectionReason reason; + + @Override + public String impId() { + return bid.getBid().getImpid(); + } + + @Override + public BidRejectionReason reason() { + return reason; + } + + @Override + public String seat() { + return bid.getSeat(); + } + + public String bidId() { + return bid.getBid().getId(); + } +} 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..e44f3e78150 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java @@ -1,36 +1,135 @@ package org.prebid.server.auction.model; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import org.prebid.server.bidder.model.BidderError; +import java.util.Arrays; + +/** + * 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); - public final int code; + /** + * 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), + + /** + * This impression not sent to the bid adapter because the impression or the bidder was removed from the request. + */ + REQUEST_BLOCKED_OPTIMIZED(203), + + /** + * 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), + + /** + * The bidder returns a bid that doesn't meet the price deal floor. + */ + RESPONSE_REJECTED_BELOW_DEAL_FLOOR(304), + + /** + * 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); + + private final int code; BidRejectionReason(int code) { this.code = code; } - @JsonValue - private int getValue() { - return code; + @JsonCreator + public static BidRejectionReason fromStatusCode(int code) { + return Arrays.stream(values()) + .filter(e -> e.code == code) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Invalid bid rejection reason: " + 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; - }; + @JsonValue + public int getValue() { + return code; } } 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..c9fd9adf00b 100644 --- a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java +++ b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java @@ -1,15 +1,18 @@ 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.ArrayList; import java.util.Collection; +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; @@ -18,73 +21,134 @@ public class BidRejectionTracker { private static final Logger logger = LoggerFactory.getLogger(BidRejectionTracker.class); - private static final ConditionalLogger MULTIPLE_BID_REJECTIONS_LOGGER = + private static final ConditionalLogger bidRejectionsLogger = new ConditionalLogger("multiple-bid-rejections", logger); - private static final String WARNING_TEMPLATE = - "Bid with imp id: %s for bidder: %s rejected due to: %s, but has been already rejected"; + private static final String MULTIPLE_REJECTIONS_WARNING_TEMPLATE = + "Warning: bidder %s on imp %s responded with multiple nonbid reasons."; + + private static final String INCONSISTENT_RESPONSES_WARNING_TEMPLATE = + "Warning: Inconsistent responses from bidder %s on imp %s: both bids and nonbids."; private final double logSamplingRate; private final String bidder; private final Set involvedImpIds; - private final Set succeededImpIds; - private final Map rejectedImpIds; + private final Map> succeededBidsIds; + private final Map> rejections; public BidRejectionTracker(String bidder, Set involvedImpIds, double logSamplingRate) { this.bidder = bidder; this.involvedImpIds = new HashSet<>(involvedImpIds); this.logSamplingRate = logSamplingRate; - succeededImpIds = new HashSet<>(); - rejectedImpIds = new HashMap<>(); + succeededBidsIds = new HashMap<>(); + rejections = new HashMap<>(); } - public void succeed(String impId) { - if (involvedImpIds.contains(impId)) { - succeededImpIds.add(impId); - rejectedImpIds.remove(impId); - } + public BidRejectionTracker(BidRejectionTracker anotherTracker, Set additionalImpIds) { + this.bidder = anotherTracker.bidder; + this.logSamplingRate = anotherTracker.logSamplingRate; + this.involvedImpIds = new HashSet<>(anotherTracker.involvedImpIds); + this.involvedImpIds.addAll(additionalImpIds); + + this.succeededBidsIds = new HashMap<>(anotherTracker.succeededBidsIds); + this.rejections = new HashMap<>(anotherTracker.rejections); } public void succeed(Collection bids) { bids.stream() .map(BidderBid::getBid) .filter(Objects::nonNull) - .map(Bid::getImpid) - .filter(Objects::nonNull) .forEach(this::succeed); } + private void succeed(Bid bid) { + final String bidId = bid.getId(); + final String impId = bid.getImpid(); + if (involvedImpIds.contains(impId)) { + succeededBidsIds.computeIfAbsent(impId, key -> new HashSet<>()).add(bidId); + if (rejections.containsKey(impId)) { + bidRejectionsLogger.warn( + INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId), + logSamplingRate); + } + } + } + public void restoreFromRejection(Collection bids) { succeed(bids); } - public void reject(String impId, BidRejectionReason reason) { - if (involvedImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) { - rejectedImpIds.put(impId, reason); - succeededImpIds.remove(impId); - } else if (rejectedImpIds.containsKey(impId)) { - MULTIPLE_BID_REJECTIONS_LOGGER.warn( - WARNING_TEMPLATE.formatted(impId, bidder, reason), logSamplingRate); + public void reject(Collection rejections) { + rejections.forEach(this::reject); + } + + public void reject(Rejection rejection) { + if (rejection instanceof ImpRejection && rejection.reason().getValue() >= 300) { + logger.warn("The rejected imp {} with the code {} equal to or higher than 300 assumes " + + "that there is a rejected bid that shouldn't be lost"); + return; + } + + final String impId = rejection.impId(); + if (involvedImpIds.contains(impId)) { + if (rejections.containsKey(impId)) { + bidRejectionsLogger.warn( + MULTIPLE_REJECTIONS_WARNING_TEMPLATE.formatted(bidder, impId), logSamplingRate); + } + + rejections.computeIfAbsent(impId, key -> new ArrayList<>()) + .add(rejection instanceof ImpRejection + ? ImpRejection.of(bidder, rejection.impId(), rejection.reason()) + : rejection); + + if (succeededBidsIds.containsKey(impId)) { + final String bidId = rejection instanceof BidRejection ? ((BidRejection) rejection).bidId() : null; + final Set succeededBids = succeededBidsIds.get(impId); + final boolean removed = bidId == null || succeededBids.remove(bidId); + if (removed && !succeededBids.isEmpty()) { + bidRejectionsLogger.warn( + INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId), + logSamplingRate); + } + } } } - public void reject(Collection impIds, BidRejectionReason reason) { - impIds.forEach(impId -> reject(impId, reason)); + public void rejectImps(Collection impIds, BidRejectionReason reason) { + impIds.forEach(impId -> reject(ImpRejection.of(impId, reason))); } public void rejectAll(BidRejectionReason reason) { - involvedImpIds.forEach(impId -> reject(impId, reason)); + involvedImpIds.forEach(impId -> reject(ImpRejection.of(impId, reason))); + } + + public Set getRejected() { + final Set rejectedResult = new HashSet<>(); + for (String impId : involvedImpIds) { + final Set succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet()); + if (succeededBids.isEmpty()) { + if (rejections.containsKey(impId)) { + rejectedResult.add(rejections.get(impId).getFirst()); + } else { + rejectedResult.add(ImpRejection.of(bidder, impId, BidRejectionReason.NO_BID)); + } + } + } + + return rejectedResult; } - public Map getRejectionReasons() { - final Map missingImpIds = new HashMap<>(); + public Map> getAllRejected() { + final Map> missingImpIds = new HashMap<>(); for (String impId : involvedImpIds) { - if (!succeededImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) { - missingImpIds.put(impId, BidRejectionReason.NO_BID); + final Set succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet()); + if (succeededBids.isEmpty() && !rejections.containsKey(impId)) { + missingImpIds.computeIfAbsent(impId, key -> new ArrayList<>()) + .add(ImpRejection.of(bidder, impId, BidRejectionReason.NO_BID)); } } - return MapUtil.merge(missingImpIds, rejectedImpIds); + return MapUtil.merge(missingImpIds, rejections); } } diff --git a/src/main/java/org/prebid/server/auction/model/BidderPrivacyResult.java b/src/main/java/org/prebid/server/auction/model/BidderPrivacyResult.java index 460ab32be11..68366064c01 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderPrivacyResult.java +++ b/src/main/java/org/prebid/server/auction/model/BidderPrivacyResult.java @@ -19,4 +19,3 @@ public class BidderPrivacyResult { boolean blockedAnalyticsByTcf; } - 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..1e2ac58a8db 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderRequest.java +++ b/src/main/java/org/prebid/server/auction/model/BidderRequest.java @@ -1,12 +1,11 @@ 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 org.prebid.server.bidder.model.Price; -import java.util.List; import java.util.Map; @Builder(toBuilder = true) @@ -19,10 +18,10 @@ public class BidderRequest { String storedResponse; - Map> impIdToDeals; - BidRequest bidRequest; + Map originalPriceFloors; + public BidderRequest with(BidRequest bidRequest) { return toBuilder().bidRequest(bidRequest).build(); } diff --git a/src/main/java/org/prebid/server/auction/model/BidderResponse.java b/src/main/java/org/prebid/server/auction/model/BidderResponse.java index ddec8e38bff..dc74bb043b7 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderResponse.java +++ b/src/main/java/org/prebid/server/auction/model/BidderResponse.java @@ -1,14 +1,12 @@ package org.prebid.server.auction.model; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.bidder.model.BidderSeatBid; /** * Structure to pass {@link BidderSeatBid} along with bidder name and extra tracking data generated during bidding */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class BidderResponse { String bidder; diff --git a/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java b/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java index f02a9af5b72..fae362f4115 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidderResponseInfo.java @@ -1,20 +1,22 @@ package org.prebid.server.auction.model; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.bidder.model.BidderSeatBidInfo; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class BidderResponseInfo { String bidder; + String seat; + + String adapterCode; + BidderSeatBidInfo seatBid; int responseTime; public BidderResponseInfo with(BidderSeatBidInfo seatBid) { - return of(this.bidder, seatBid, this.responseTime); + return of(this.bidder, this.seat, this.adapterCode, seatBid, this.responseTime); } } diff --git a/src/main/java/org/prebid/server/auction/model/ImpRejection.java b/src/main/java/org/prebid/server/auction/model/ImpRejection.java new file mode 100644 index 00000000000..578a9f0ccfc --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/ImpRejection.java @@ -0,0 +1,20 @@ +package org.prebid.server.auction.model; + +import lombok.Value; +import lombok.experimental.Accessors; + +@Value(staticConstructor = "of") +@Accessors(fluent = true) +public class ImpRejection implements Rejection { + + String seat; + + String impId; + + BidRejectionReason reason; + + public static ImpRejection of(String impId, BidRejectionReason reason) { + return ImpRejection.of(null, impId, reason); + } + +} diff --git a/src/main/java/org/prebid/server/auction/model/PaaFormat.java b/src/main/java/org/prebid/server/auction/model/PaaFormat.java new file mode 100644 index 00000000000..8367b61c0f2 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/PaaFormat.java @@ -0,0 +1,12 @@ +package org.prebid.server.auction.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum PaaFormat { + + @JsonProperty("original") + ORIGINAL, + + @JsonProperty("iab") + IAB +} diff --git a/src/main/java/org/prebid/server/auction/model/Rejection.java b/src/main/java/org/prebid/server/auction/model/Rejection.java new file mode 100644 index 00000000000..9604cffcea2 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/Rejection.java @@ -0,0 +1,11 @@ +package org.prebid.server.auction.model; + +public interface Rejection { + + String seat(); + + String impId(); + + BidRejectionReason reason(); + +} diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java index d045b0ceadc..05c451bf863 100644 --- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java +++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java b/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java index 4b3dfa736f3..8f72806ae92 100644 --- a/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java +++ b/src/main/java/org/prebid/server/auction/model/StoredResponseResult.java @@ -2,14 +2,12 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.SeatBid; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class StoredResponseResult { List requiredRequestImps; diff --git a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java index a85b1041971..60f453a942f 100644 --- a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java +++ b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java @@ -9,11 +9,11 @@ public class TargetingInfo { String bidderCode; + String seat; + boolean isTargetingEnabled; boolean isWinningBid; - boolean isBidderWinningBid; - boolean isAddTargetBidderCode; } diff --git a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java index b8379056afa..87c390e6b2e 100644 --- a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java +++ b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java @@ -1,7 +1,7 @@ package org.prebid.server.auction.model; import lombok.Value; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; @Value(staticConstructor = "of") public class TimeoutContext { diff --git a/src/main/java/org/prebid/server/auction/model/Tuple2.java b/src/main/java/org/prebid/server/auction/model/Tuple2.java index 16d641f40c3..560f5dab4fa 100644 --- a/src/main/java/org/prebid/server/auction/model/Tuple2.java +++ b/src/main/java/org/prebid/server/auction/model/Tuple2.java @@ -1,10 +1,8 @@ package org.prebid.server.auction.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class Tuple2 { L left; diff --git a/src/main/java/org/prebid/server/auction/model/Tuple3.java b/src/main/java/org/prebid/server/auction/model/Tuple3.java deleted file mode 100644 index bb26c145dc3..00000000000 --- a/src/main/java/org/prebid/server/auction/model/Tuple3.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.auction.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class Tuple3 { - - L left; - - M middle; - - R right; -} diff --git a/src/main/java/org/prebid/server/auction/model/WithPodErrors.java b/src/main/java/org/prebid/server/auction/model/WithPodErrors.java index 6238637afd2..aa6582ff39c 100644 --- a/src/main/java/org/prebid/server/auction/model/WithPodErrors.java +++ b/src/main/java/org/prebid/server/auction/model/WithPodErrors.java @@ -1,13 +1,11 @@ package org.prebid.server.auction.model; import com.iab.openrtb.request.video.PodError; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class WithPodErrors { T data; diff --git a/src/main/java/org/prebid/server/auction/model/debug/BidderDebugContext.java b/src/main/java/org/prebid/server/auction/model/debug/BidderDebugContext.java deleted file mode 100644 index f3d2e07dd1a..00000000000 --- a/src/main/java/org/prebid/server/auction/model/debug/BidderDebugContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.auction.model.debug; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class BidderDebugContext { - - boolean debugEnabled; - - boolean shouldReturnAllBidStatuses; -} 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/contextfactory/CookieSyncPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java index 55e32fd1372..0e1e6cbab13 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java index 3e81b3fcf4f..b637e656290 100644 --- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java +++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java @@ -6,7 +6,7 @@ import org.prebid.server.auction.ImplicitParametersExtractor; import org.prebid.server.auction.IpAddressHelper; import org.prebid.server.auction.model.IpAddress; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.metric.MetricName; import org.prebid.server.privacy.PrivacyExtractor; import org.prebid.server.privacy.gdpr.TcfDefinerService; 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..009b14ade2a 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 @@ -12,6 +12,7 @@ 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.PrivacyEnforcementServiceActivityInvocationPayload; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; @@ -21,7 +22,7 @@ import java.util.Objects; import java.util.Optional; -public class ActivityEnforcement { +public class ActivityEnforcement implements PrivacyEnforcement { private final UserFpdActivityMask userFpdActivityMask; @@ -29,17 +30,19 @@ public ActivityEnforcement(UserFpdActivityMask userFpdActivityMask) { this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } - public Future> enforce(List bidderPrivacyResults, - AuctionContext auctionContext) { + @Override + public Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results) { - final List results = bidderPrivacyResults.stream() + final List enforcedResults = results.stream() .map(bidderPrivacyResult -> applyActivityRestrictions( bidderPrivacyResult, auctionContext.getActivityInfrastructure(), auctionContext.getBidRequest())) .toList(); - return Future.succeededFuture(results); + return Future.succeededFuture(enforcedResults); } private BidderPrivacyResult applyActivityRestrictions(BidderPrivacyResult bidderPrivacyResult, @@ -57,15 +60,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..dd9c9b0e580 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 @@ -1,11 +1,8 @@ package org.prebid.server.auction.privacy.enforcement; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; import io.vertx.core.Future; -import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCcpaMask; @@ -22,12 +19,12 @@ import java.util.Collections; 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.stream.Collectors; -public class CcpaEnforcement { +public class CcpaEnforcement implements PrivacyEnforcement { private static final String CATCH_ALL_BIDDERS = "*"; @@ -47,54 +44,58 @@ public CcpaEnforcement(UserFpdCcpaMask userFpdCcpaMask, this.ccpaEnforce = ccpaEnforce; } + @Override public Future> enforce(AuctionContext auctionContext, - Map bidderToUser, - BidderAliases aliases) { + BidderAliases aliases, + List results) { 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(results, 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(); - } + final List enforcedResults = results.stream() + .map(result -> enforcedBidders.contains(result.getRequestBidder()) ? maskCcpa(result) : result) + .toList(); - public boolean isCcpaEnforced(Ccpa ccpa, Account account) { - return isCcpaEnforced(ccpa, account, null); + return Future.succeededFuture(enforcedResults); } - private boolean isCcpaEnforced(Ccpa ccpa, Account account, MetricName requestType) { - return ccpa.isEnforced() && isCcpaEnabled(account, requestType); + public boolean isCcpaEnforced(Ccpa ccpa, Account account) { + return ccpa.isEnforced() && isCcpaEnabled(account, null); } private boolean isCcpaEnabled(Account account, MetricName requestType) { final Optional accountCcpaConfig = Optional.ofNullable(account.getPrivacy()) .map(AccountPrivacyConfig::getCcpa); - return ObjectUtils.firstNonNull( - accountCcpaConfig - .map(AccountCcpaConfig::getEnabledForRequestType) - .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType)) - .orElse(null), - accountCcpaConfig - .map(AccountCcpaConfig::getEnabled) - .orElse(null), - ccpaEnforce); + return accountCcpaConfig + .map(AccountCcpaConfig::getEnabledForRequestType) + .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType)) + .or(() -> accountCcpaConfig.map(AccountCcpaConfig::getEnabled)) + .orElse(ccpaEnforce); } - private Set extractCcpaEnforcedBidders(Set bidders, BidRequest bidRequest, BidderAliases aliases) { - final Set ccpaEnforcedBidders = new HashSet<>(bidders); + private Set extractCcpaEnforcedBidders(List results, + BidRequest bidRequest, + BidderAliases aliases) { + + final Set ccpaEnforcedBidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toCollection(HashSet::new)); + final List nosaleBidders = Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getNosale) @@ -112,14 +113,11 @@ private Set extractCcpaEnforcedBidders(Set bidders, BidRequest b return ccpaEnforcedBidders; } - private List maskCcpa(Map bidderToUser, Set bidders, Device device) { - final Device maskedDevice = userFpdCcpaMask.maskDevice(device); - return bidders.stream() - .map(bidder -> BidderPrivacyResult.builder() - .requestBidder(bidder) - .user(userFpdCcpaMask.maskUser(bidderToUser.get(bidder))) - .device(maskedDevice) - .build()) - .toList(); + private BidderPrivacyResult maskCcpa(BidderPrivacyResult result) { + return BidderPrivacyResult.builder() + .requestBidder(result.getRequestBidder()) + .user(userFpdCcpaMask.maskUser(result.getUser())) + .device(userFpdCcpaMask.maskDevice(result.getDevice())) + .build(); } } 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..8b273b665df 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 @@ -1,18 +1,18 @@ package org.prebid.server.auction.privacy.enforcement; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask; import org.prebid.server.metric.Metrics; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; -public class CoppaEnforcement { +public class CoppaEnforcement implements PrivacyEnforcement { private final UserFpdCoppaMask userFpdCoppaMask; private final Metrics metrics; @@ -22,23 +22,34 @@ public CoppaEnforcement(UserFpdCoppaMask userFpdCoppaMask, Metrics metrics) { this.metrics = Objects.requireNonNull(metrics); } - public boolean isApplicable(AuctionContext auctionContext) { - return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1; - } + @Override + public Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results) { + + if (!isApplicable(auctionContext)) { + return Future.succeededFuture(results); + } + + final Set bidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toSet()); - public Future> enforce(AuctionContext auctionContext, Map bidderToUser) { - metrics.updatePrivacyCoppaMetric(); - return Future.succeededFuture(results(bidderToUser, auctionContext.getBidRequest().getDevice())); + metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidders); + return Future.succeededFuture(enforce(results)); } - private List results(Map bidderToUser, Device device) { - final Device maskedDevice = userFpdCoppaMask.maskDevice(device); - return bidderToUser.entrySet().stream() - .map(bidderAndUser -> BidderPrivacyResult.builder() - .requestBidder(bidderAndUser.getKey()) - .user(userFpdCoppaMask.maskUser(bidderAndUser.getValue())) - .device(maskedDevice) + private List enforce(List results) { + return results.stream() + .map(result -> BidderPrivacyResult.builder() + .requestBidder(result.getRequestBidder()) + .user(userFpdCoppaMask.maskUser(result.getUser())) + .device(userFpdCoppaMask.maskDevice(result.getDevice())) .build()) .toList(); } + + private static boolean isApplicable(AuctionContext auctionContext) { + return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1; + } } diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java new file mode 100644 index 00000000000..6ff56836e79 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java @@ -0,0 +1,15 @@ +package org.prebid.server.auction.privacy.enforcement; + +import io.vertx.core.Future; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.BidderPrivacyResult; + +import java.util.List; + +public interface PrivacyEnforcement { + + Future> enforce(AuctionContext auctionContext, + BidderAliases aliases, + List results); +} diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java index 3f4e4055dca..b14ff44444c 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java @@ -1,62 +1,47 @@ package org.prebid.server.auction.privacy.enforcement; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; -import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; -import org.prebid.server.util.ListUtil; +import org.apache.commons.lang3.tuple.Pair; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Service provides masking for OpenRTB client sensitive information. */ public class PrivacyEnforcementService { - private final CoppaEnforcement coppaEnforcement; - private final CcpaEnforcement ccpaEnforcement; - private final TcfEnforcement tcfEnforcement; - private final ActivityEnforcement activityEnforcement; + private final List enforcements; - public PrivacyEnforcementService(CoppaEnforcement coppaEnforcement, - CcpaEnforcement ccpaEnforcement, - TcfEnforcement tcfEnforcement, - ActivityEnforcement activityEnforcement) { - - this.coppaEnforcement = Objects.requireNonNull(coppaEnforcement); - this.ccpaEnforcement = Objects.requireNonNull(ccpaEnforcement); - this.tcfEnforcement = Objects.requireNonNull(tcfEnforcement); - this.activityEnforcement = Objects.requireNonNull(activityEnforcement); + public PrivacyEnforcementService(final List enforcements) { + this.enforcements = Objects.requireNonNull(enforcements); } public Future> mask(AuctionContext auctionContext, - Map bidderToUser, + Map> bidderToUserAndDevice, BidderAliases aliases) { - // For now, COPPA masking all values, so we can omit TCF masking. - return coppaEnforcement.isApplicable(auctionContext) - ? coppaEnforcement.enforce(auctionContext, bidderToUser) - : ccpaEnforcement.enforce(auctionContext, bidderToUser, aliases) - .compose(ccpaResult -> tcfEnforcement.enforce( - auctionContext, - bidderToUser, - biddersToApplyTcf(bidderToUser.keySet(), ccpaResult), - aliases) - .map(tcfResult -> ListUtil.union(ccpaResult, tcfResult))) - .compose(bidderPrivacyResults -> activityEnforcement.enforce(bidderPrivacyResults, auctionContext)); - } + final List initialResults = bidderToUserAndDevice.entrySet().stream() + .map(entry -> BidderPrivacyResult.builder() + .requestBidder(entry.getKey()) + .user(entry.getValue().getLeft()) + .device(entry.getValue().getRight()) + .build()) + .toList(); + + Future> composedResult = Future.succeededFuture(initialResults); - private static Set biddersToApplyTcf(Set bidders, List ccpaResult) { - final Set biddersToApplyTcf = new HashSet<>(bidders); - ccpaResult.stream() - .map(BidderPrivacyResult::getRequestBidder) - .forEach(biddersToApplyTcf::remove); + for (PrivacyEnforcement enforcement : enforcements) { + composedResult = composedResult.compose( + results -> enforcement.enforce(auctionContext, aliases, results)); + } - return biddersToApplyTcf; + return composedResult; } } 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..4bac7952375 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,15 @@ 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.auction.BidderAliases; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.aliases.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; @@ -29,26 +29,24 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; -public class TcfEnforcement { +public class TcfEnforcement implements PrivacyEnforcement { private static final Logger logger = LoggerFactory.getLogger(TcfEnforcement.class); private final TcfDefinerService tcfDefinerService; private final UserFpdTcfMask userFpdTcfMask; - private final BidderCatalog bidderCatalog; private final Metrics metrics; private final boolean lmtEnforce; public TcfEnforcement(TcfDefinerService tcfDefinerService, UserFpdTcfMask userFpdTcfMask, - BidderCatalog bidderCatalog, Metrics metrics, boolean lmtEnforce) { this.tcfDefinerService = Objects.requireNonNull(tcfDefinerService); this.userFpdTcfMask = Objects.requireNonNull(userFpdTcfMask); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.metrics = Objects.requireNonNull(metrics); this.lmtEnforce = lmtEnforce; } @@ -58,23 +56,32 @@ public Future> enforce(Set vendo .map(TcfResponse::getActions); } + @Override public Future> enforce(AuctionContext auctionContext, - Map bidderToUser, - Set bidders, - BidderAliases aliases) { + BidderAliases aliases, + List results) { - final Device device = auctionContext.getBidRequest().getDevice(); - final AccountGdprConfig accountGdprConfig = accountGdprConfig(auctionContext.getAccount()); final MetricName requestType = auctionContext.getRequestTypeMetric(); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + final Account account = auctionContext.getAccount(); + final Set bidders = results.stream() + .map(BidderPrivacyResult::getRequestBidder) + .collect(Collectors.toSet()); return tcfDefinerService.resultForBidderNames( bidders, - VendorIdResolver.of(aliases, bidderCatalog), + VendorIdResolver.of(aliases), auctionContext.getPrivacyContext().getTcfContext(), - accountGdprConfig) + accountGdprConfig(account)) .map(TcfResponse::getActions) - .map(enforcements -> updateMetrics(enforcements, aliases, requestType, bidderToUser, device)) - .map(enforcements -> bidderToPrivacyResult(enforcements, bidders, bidderToUser, device)); + .map(enforcements -> updateMetrics( + activityInfrastructure, + enforcements, + aliases, + requestType, + results, + account)) + .map(enforcements -> applyEnforcements(enforcements, results)); } private static AccountGdprConfig accountGdprConfig(Account account) { @@ -82,45 +89,54 @@ private static AccountGdprConfig accountGdprConfig(Account account) { return privacyConfig != null ? privacyConfig.getGdpr() : null; } - private Map updateMetrics(Map enforcements, + private static boolean hasBuyerUid(User user) { + return user != null && user.getBuyeruid() != null; + } + + private Map updateMetrics(ActivityInfrastructure activityInfrastructure, + Map enforcements, BidderAliases aliases, MetricName requestType, - Map bidderToUser, - Device device) { + List results, + Account account) { // 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()) { - final String bidder = bidderEnforcement.getKey(); - final PrivacyEnforcementAction enforcement = bidderEnforcement.getValue(); - final User user = bidderToUser.get(bidder); + for (BidderPrivacyResult result : results) { + final String bidder = result.getRequestBidder(); + final User user = result.getUser(); + final Device device = result.getDevice(); + final PrivacyEnforcementAction enforcement = enforcements.get(bidder); final boolean requestBlocked = enforcement.isBlockBidderRequest(); final boolean ufpdRemoved = !requestBlocked && ((enforcement.isRemoveUserFpd() && shouldRemoveUserData(user)) || (enforcement.isMaskDeviceInfo() && shouldRemoveDeviceData(device))); + final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device); final boolean uidsRemoved = !requestBlocked && enforcement.isRemoveUserIds() && shouldRemoveUids(user); final boolean geoMasked = !requestBlocked && enforcement.isMaskGeo() && shouldMaskGeo(user, device); final boolean analyticsBlocked = !requestBlocked && enforcement.isBlockAnalyticsReport(); - metrics.updateAuctionTcfMetrics( + metrics.updateAuctionTcfAndLmtMetrics( + activityInfrastructure, aliases.resolveBidder(bidder), requestType, ufpdRemoved, uidsRemoved, geoMasked, analyticsBlocked, - requestBlocked); + requestBlocked, + isLmtEnforcedAndEnabled); + + if (hasBuyerUid(user) && ufpdRemoved) { + metrics.updateAdapterRequestBuyerUidScrubbedMetrics(bidder, account); + } if (ufpdRemoved) { logger.warn("The UFPD fields have been removed due to a consent check."); } } - if (isLmtEnforcedAndEnabled(device)) { - metrics.updatePrivacyLmtMetric(); - } - return enforcements; } @@ -156,32 +172,20 @@ private boolean isLmtEnforcedAndEnabled(Device device) { return lmtEnforce && device != null && Objects.equals(device.getLmt(), 1); } - private List bidderToPrivacyResult(Map bidderToEnforcement, - Set bidders, - Map bidderToUser, - Device device) { - - final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device); + private List applyEnforcements(Map enforcements, + List results) { - return bidders.stream() - .map(bidder -> createBidderPrivacyResult( - bidder, - bidderToUser.get(bidder), - device, - bidderToEnforcement, - isLmtEnabled)) + return results.stream() + .map(result -> applyEnforcement(enforcements.get(result.getRequestBidder()), result)) .toList(); } - private BidderPrivacyResult createBidderPrivacyResult(String bidder, - User user, - Device device, - Map bidderToEnforcement, - boolean isLmtEnabled) { + private BidderPrivacyResult applyEnforcement(PrivacyEnforcementAction enforcement, BidderPrivacyResult result) { + final String bidder = result.getRequestBidder(); + + final boolean blockBidderRequest = enforcement.isBlockBidderRequest(); + final boolean blockAnalyticsReport = enforcement.isBlockAnalyticsReport(); - final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder); - final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest(); - final boolean blockAnalyticsReport = privacyEnforcementAction.isBlockAnalyticsReport(); if (blockBidderRequest) { return BidderPrivacyResult.builder() .requestBidder(bidder) @@ -190,14 +194,18 @@ private BidderPrivacyResult createBidderPrivacyResult(String bidder, .build(); } - final boolean maskUserFpd = privacyEnforcementAction.isRemoveUserFpd() || isLmtEnabled; - final boolean maskUserIds = privacyEnforcementAction.isRemoveUserIds() || isLmtEnabled; - final boolean maskGeo = privacyEnforcementAction.isMaskGeo() || isLmtEnabled; - final Set eidExceptions = privacyEnforcementAction.getEidExceptions(); - final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, maskGeo, eidExceptions); + final User user = result.getUser(); + final Device device = result.getDevice(); - final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtEnabled; - final boolean maskDeviceInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtEnabled; + final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device); + final boolean maskUserFpd = enforcement.isRemoveUserFpd() || isLmtEnabled; + final boolean maskUserIds = enforcement.isRemoveUserIds() || isLmtEnabled; + final boolean maskGeo = enforcement.isMaskGeo() || isLmtEnabled; + final Set eidExceptions = enforcement.getEidExceptions(); + final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, eidExceptions); + + final boolean maskIp = enforcement.isMaskDeviceIp() || isLmtEnabled; + final boolean maskDeviceInfo = enforcement.isMaskDeviceInfo() || isLmtEnabled; final Device maskedDevice = userFpdTcfMask.maskDevice(device, maskIp, maskGeo, maskDeviceInfo); return BidderPrivacyResult.builder() 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..e1c5e4240ce 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -19,10 +19,12 @@ 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; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.ConsentType; @@ -51,6 +53,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 +62,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 { @@ -87,6 +91,7 @@ public class AmpRequestFactory { private final Ortb2RequestFactory ortb2RequestFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final AmpGppService gppService; private final OrtbTypesResolver ortbTypesResolver; @@ -96,9 +101,11 @@ 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, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, AmpGppService gppService, OrtbTypesResolver ortbTypesResolver, @@ -107,10 +114,12 @@ 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); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.gppService = Objects.requireNonNull(gppService); this.ortbTypesResolver = Objects.requireNonNull(ortbTypesResolver); @@ -120,13 +129,14 @@ public AmpRequestFactory(Ortb2RequestFactory ortb2RequestFactory, this.debugResolver = Objects.requireNonNull(debugResolver); this.ampPrivacyContextFactory = Objects.requireNonNull(ampPrivacyContextFactory); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); } /** * Creates {@link AuctionContext} based on {@link RoutingContext}. */ public Future fromRequest(RoutingContext routingContext, long startTime) { - final String body = routingContext.getBodyAsString(); + final String body = routingContext.body().asString(); final AuctionContext initialAuctionContext = ortb2RequestFactory.createAuctionContext( Endpoint.openrtb2_amp, MetricName.amp); @@ -142,6 +152,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 +170,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 +197,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() @@ -263,6 +278,7 @@ private static User createUser(ConsentParam consentParam, String addtlConsent) { final ExtUser extUser = consentedProvidersSettings != null ? ExtUser.builder() + .deprecatedConsentedProvidersSettings(consentedProvidersSettings) .consentedProvidersSettings(consentedProvidersSettings) .build() : null; @@ -334,7 +350,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 +365,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(); } } @@ -389,15 +411,23 @@ private Future updateBidRequest(AuctionContext auctionContext) { final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); return storedRequestProcessor.processAmpRequest(accountId, storedRequestId, receivedBidRequest) + .compose(bidRequest -> profilesProcessor.process(auctionContext, bidRequest)) .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.limitImpressions( + account, + resolvedBidRequest, + auctionContext.getDebugWarnings())) .compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest( + account, resolvedBidRequest, auctionContext.getHttpRequest(), + auctionContext.getDebugContext(), auctionContext.getDebugWarnings())); } @@ -443,10 +473,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 +507,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 +530,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 +707,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 +720,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), @@ -708,18 +740,17 @@ private ExtRequest extRequest(BidRequest bidRequest, } /** - * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's + * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of its * 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 +774,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..d873af3e696 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -7,15 +7,18 @@ 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; -import org.prebid.server.auction.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; @@ -38,6 +41,7 @@ public class AuctionRequestFactory { private final long maxRequestSize; private final Ortb2RequestFactory ortb2RequestFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final AuctionGppService gppService; private final CookieDeprecationService cookieDeprecationService; @@ -48,12 +52,15 @@ public class AuctionRequestFactory { private final DebugResolver debugResolver; private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; + private final GeoLocationServiceWrapper geoLocationServiceWrapper; + private final BidAdjustmentsEnricher bidAdjustmentsEnricher; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); public AuctionRequestFactory(long maxRequestSize, Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, AuctionGppService gppService, CookieDeprecationService cookieDeprecationService, @@ -63,11 +70,14 @@ public AuctionRequestFactory(long maxRequestSize, OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsEnricher bidAdjustmentsEnricher) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.gppService = Objects.requireNonNull(gppService); this.cookieDeprecationService = Objects.requireNonNull(cookieDeprecationService); @@ -78,12 +88,14 @@ public AuctionRequestFactory(long maxRequestSize, this.auctionPrivacyContextFactory = Objects.requireNonNull(auctionPrivacyContextFactory); this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); + this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); + this.bidAdjustmentsEnricher = Objects.requireNonNull(bidAdjustmentsEnricher); } /** - * 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 +108,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,23 +147,21 @@ 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.executeProcessedAuctionRequestHooks(auctionContext) + .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) .map(auctionContext::with)) - .compose(ortb2RequestFactory::populateUserAdditionalInfo) + .map(auctionContext -> auctionContext.with(bidAdjustmentsEnricher.enrichBidRequest(auctionContext))) - .map(ortb2RequestFactory::enrichWithPriceFloors) + .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) + .map(auctionContext::with)) - .map(auctionContext -> ortb2RequestFactory.updateTimeout(auctionContext, startTime)) + .map(ortb2RequestFactory::updateTimeout) .recover(ortb2RequestFactory::restoreResultFromRejection); } private String extractAndValidateBody(RoutingContext routingContext) { - final String body = routingContext.getBodyAsString(); + final String body = routingContext.body().asString(); if (body == null) { throw new InvalidRequestException("Incoming request has no body"); } @@ -216,14 +240,14 @@ 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.limitImpressions(account, bidRequest, debugWarnings)) .compose(bidRequest -> ortb2RequestFactory.validateRequest( - bidRequest, - auctionContext.getHttpRequest(), - debugWarnings)) + account, bidRequest, httpRequest, auctionContext.getDebugContext(), debugWarnings)) .map(interstitialProcessor::process); } @@ -232,11 +256,12 @@ private Future updateBidRequest(AuctionStoredResult auctionStoredRes final boolean hasStoredBidRequest = auctionStoredResult.hasStoredBidRequest(); - return Future.succeededFuture(auctionStoredResult.bidRequest()) + return profilesProcessor.process(auctionContext, auctionStoredResult.bidRequest()) .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..90c049db606 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.aliases.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); @@ -358,7 +369,7 @@ private static Integer resolveLmtForIos14Minor0And1(Device device) { final String ifa = device.getIfa(); final Integer lmt = device.getLmt(); - if (StringUtils.isEmpty(ifa) || ifa.equals("00000000-0000-0000-0000-000000000000")) { + if (StringUtils.isEmpty(ifa) || "00000000-0000-0000-0000-000000000000".equals(ifa)) { return !Objects.equals(lmt, 1) ? 1 : null; } @@ -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..1604783c3b8 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,18 @@ 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.Imp; 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; @@ -20,23 +23,21 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.activity.infrastructure.creator.ActivityInfrastructureCreator; import org.prebid.server.auction.IpAddressHelper; -import org.prebid.server.auction.StoredRequestProcessor; import org.prebid.server.auction.TimeoutResolver; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.model.IpAddress; 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.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.CountryCodeMapper; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.execution.HookStageExecutor; @@ -45,6 +46,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 +56,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,82 +77,74 @@ 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 { private static final Logger logger = LoggerFactory.getLogger(Ortb2RequestFactory.class); - 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 static final ConditionalLogger emptyAccountLogger = new ConditionalLogger("empty_account", logger); + private static final ConditionalLogger unknownAccountLogger = 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; private final TimeoutResolver timeoutResolver; private final TimeoutFactory timeoutFactory; private final StoredRequestProcessor storedRequestProcessor; + private final ProfilesProcessor profilesProcessor; 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, TimeoutResolver timeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, 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); this.timeoutResolver = Objects.requireNonNull(timeoutResolver); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); + this.profilesProcessor = Objects.requireNonNull(profilesProcessor); 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 +155,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 +170,6 @@ public AuctionContext enrichAuctionContext(AuctionContext auctionContext, .uidsCookie(uidsCookieService.parseFromRequest(httpRequest)) .bidRequest(bidRequest) .timeoutContext(TimeoutContext.of(startTime, timeout(bidRequest, startTime), timeoutAdjustmentFactor)) - .deepDebugLog(createDeepDebugLog(bidRequest)) .build(); } @@ -194,8 +186,8 @@ private Future fetchAccount(AuctionContext auctionContext, boolean isLo final Timeout timeout = auctionContext.getTimeoutContext().getTimeout(); final HttpRequestContext httpRequest = auctionContext.getHttpRequest(); - return findAccountIdFrom(bidRequest, isLookupStoredRequest) - .map(this::validateIfAccountBlacklisted) + return findAccountIdFrom(auctionContext, bidRequest, isLookupStoredRequest) + .map(this::validateIfAccountBlocklisted) .compose(accountId -> loadAccount(timeout, httpRequest, accountId)); } @@ -206,11 +198,31 @@ public Future activityInfrastructureFrom(AuctionContext auctionContext.getDebugContext().getTraceLevel())); } - public Future validateRequest(BidRequest bidRequest, + public Future limitImpressions(Account account, BidRequest bidRequest, List warnings) { + final List imps = bidRequest.getImp(); + final int impsLimit = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getImpressionLimit) + .orElse(0); + + if (impsLimit > 0 && imps.size() > impsLimit) { + metrics.updateImpsDroppedMetric(imps.size() - impsLimit); + warnings.add(("Only first %d impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction").formatted(impsLimit)); + return Future.succeededFuture(bidRequest.toBuilder().imp(imps.subList(0, impsLimit)).build()); + } + + return Future.succeededFuture(bidRequest); + } + + public Future validateRequest(Account account, + BidRequest bidRequest, HttpRequestContext httpRequestContext, + DebugContext debugContext, List warnings) { - final ValidationResult validationResult = requestValidator.validate(bidRequest, httpRequestContext); + final ValidationResult validationResult = requestValidator.validate( + account, bidRequest, httpRequestContext, debugContext); if (validationResult.hasWarnings()) { warnings.addAll(validationResult.getWarnings()); @@ -221,7 +233,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 +310,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 +350,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(); @@ -303,7 +378,7 @@ public Future executeEntrypointHooks(RoutingContext routingC toCaseInsensitiveMultiMap(routingContext.queryParams()), toCaseInsensitiveMultiMap(routingContext.request().headers()), body, - auctionContext.getHookExecutionContext()) + auctionContext) .map(stageResult -> toHttpRequest(stageResult, routingContext, auctionContext)); } @@ -336,6 +411,7 @@ private static HttpRequestContext toHttpRequest(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(); @@ -406,45 +473,22 @@ private Timeout timeout(BidRequest bidRequest, long startTime) { return timeoutFactory.create(startTime, timeout); } - private Future findAccountIdFrom(BidRequest bidRequest, boolean isLookupStoredRequest) { - final String accountId = accountIdFrom(bidRequest); - return StringUtils.isNotBlank(accountId) || !isLookupStoredRequest - ? Future.succeededFuture(accountId) - : storedRequestProcessor.processAuctionRequest(accountId, bidRequest) - .map(storedAuctionResult -> accountIdFrom(storedAuctionResult.bidRequest())); - } - - private String validateIfAccountBlacklisted(String accountId) { - if (CollectionUtils.isNotEmpty(blacklistedAccounts) - && StringUtils.isNotBlank(accountId) - && blacklistedAccounts.contains(accountId)) { + private Future findAccountIdFrom(AuctionContext auctionContext, + BidRequest bidRequest, + boolean isLookupStoredRequest) { - throw new BlacklistedAccountException( - "Prebid-server has blacklisted Account ID: %s, please reach out to the prebid server host." - .formatted(accountId)); + final String accountId = accountIdFromBidRequest(bidRequest); + if (StringUtils.isNotBlank(accountId) || !isLookupStoredRequest) { + return Future.succeededFuture(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)); - return accountFuture - .onFailure(ignored -> metrics.updateAccountRequestRejectedByInvalidAccountMetrics(accountId)); + return accountIdFromStoredRequest(bidRequest) + .compose(id -> StringUtils.isBlank(id) + ? accountIdFromProfiles(auctionContext, bidRequest) + : Future.succeededFuture(id)); } - /** - * Extracts publisher id either from {@link BidRequest}.app.publisher or {@link BidRequest}.site.publisher. - * If neither is present returns empty string. - */ - private String accountIdFrom(BidRequest bidRequest) { + private String accountIdFromBidRequest(BidRequest bidRequest) { final App app = bidRequest.getApp(); final Publisher appPublisher = app != null ? app.getPublisher() : null; final Site site = bidRequest.getSite(); @@ -457,67 +501,81 @@ private String accountIdFrom(BidRequest bidRequest) { return ObjectUtils.defaultIfNull(publisherId, StringUtils.EMPTY); } - /** - * Resolves what value should be used as a publisher id - either taken from publisher.ext.parentAccount - * or publisher.id in this respective priority. - */ private String resolvePublisherId(Publisher publisher) { final String parentAccountId = parentAccountIdFromExtPublisher(publisher.getExt()); return ObjectUtils.defaultIfNull(parentAccountId, publisher.getId()); } - /** - * Parses publisher.ext and returns parentAccount value from it. Returns null if any parsing error occurs. - */ private String parentAccountIdFromExtPublisher(ExtPublisher extPublisher) { final ExtPublisherPrebid extPublisherPrebid = extPublisher != null ? extPublisher.getPrebid() : null; 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 Future accountIdFromStoredRequest(BidRequest bidRequest) { + return storedRequestProcessor.processAuctionRequest(StringUtils.EMPTY, bidRequest) + .map(AuctionStoredResult::bidRequest) + .map(this::accountIdFromBidRequest); } - 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 accountIdFromProfiles(AuctionContext auctionContext, BidRequest bidRequest) { + return profilesProcessor.process(auctionContext, bidRequest) + .map(this::accountIdFromBidRequest); } - private Future accountFallback(Throwable exception, - String accountId, - HttpRequestContext httpRequest) { + private String validateIfAccountBlocklisted(String accountId) { + if (CollectionUtils.isNotEmpty(blocklistedAccounts) + && StringUtils.isNotBlank(accountId) + && blocklistedAccounts.contains(accountId)) { - 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.debug("Error occurred while fetching account", exception); + throw new BlocklistedAccountException( + "Prebid-server has blocklisted Account ID: %s, please reach out to the prebid server host." + .formatted(accountId)); } - - // hide all errors occurred while fetching account - return responseForUnknownAccount(accountId); + return accountId; } - private Future responseForUnknownAccount(String accountId) { - return enforceValidAccount - ? Future.failedFuture(new UnauthorizedAccountException( - "Unauthorized account id: " + accountId, accountId)) - : Future.succeededFuture(Account.empty(accountId)); + private Future loadAccount(Timeout timeout, HttpRequestContext httpRequest, String accountId) { + if (StringUtils.isBlank(accountId)) { + emptyAccountLogger.warn(accountErrorMessage("Account not specified", httpRequest), logSamplingRate); + } + + 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.failedFuture( + new UnauthorizedAccountException("Account %s is inactive".formatted(accountId), accountId)) : Future.succeededFuture(account); } + private Future wrapFailure(Throwable exception, String accountId, HttpRequestContext httpRequest) { + if (exception instanceof UnauthorizedAccountException) { + return Future.failedFuture(exception); + } else if (exception instanceof PreBidException) { + unknownAccountLogger.warn(accountErrorMessage(exception.getMessage(), httpRequest), 100); + } else { + metrics.updateAccountRequestRejectedByFailedFetch(accountId); + logger.warn("Error occurred while fetching account: {}", exception.getMessage()); + logger.debug("Error occurred while fetching account", exception); + } + + return Future.failedFuture( + new UnauthorizedAccountException("Unauthorized account id: " + accountId, accountId)); + } + + 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) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); if (accountAuctionConfig == null) { @@ -613,9 +671,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 +704,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 +715,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 +736,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 +744,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..811db804fb9 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,24 +108,39 @@ 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())) + + .map(bidRequestWithErrors -> populatePodErrors( + bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) - .compose(bidRequest -> validateRequest( - bidRequest, - httpRequest, - initialAuctionContext.getDebugWarnings())) + .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( + initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + + .map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext))) - .map(bidRequestWithErrors -> populatePodErrors( - bidRequestWithErrors.getPodErrors(), podErrors, bidRequestWithErrors)) + .compose(auctionContext -> ortb2RequestFactory.limitImpressions( + auctionContext.getAccount(), + auctionContext.getBidRequest(), + auctionContext.getDebugWarnings()) + .map(auctionContext::with)) - .map(bidRequestWithErrors -> ortb2RequestFactory.enrichAuctionContext( - initialAuctionContext, httpRequest, bidRequestWithErrors.getData(), startTime))) + .compose(auctionContext -> ortb2RequestFactory.validateRequest( + auctionContext.getAccount(), + auctionContext.getBidRequest(), + auctionContext.getHttpRequest(), + auctionContext.getDebugContext(), + auctionContext.getDebugWarnings()) + .map(auctionContext::with)) .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)) @@ -129,17 +148,13 @@ public Future> fromRequest(RoutingContext routingC .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,8 +163,16 @@ 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(); + final String body = routingContext.body().asString(); if (body == null) { throw new InvalidRequestException("Incoming request has no body"); } @@ -310,12 +333,4 @@ private WithPodErrors fillImplicitParameters(HttpRequestContext http return WithPodErrors.of(updatedWithDebugBidRequest, bidRequestToErrors.getPodErrors()); } - - private Future> validateRequest(WithPodErrors requestWithPodErrors, - HttpRequestContext httpRequestContext, - List warnings) { - - return ortb2RequestFactory.validateRequest(requestWithPodErrors.getData(), httpRequestContext, warnings) - .map(bidRequest -> requestWithPodErrors); - } } 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/bidadjustments/BidAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java new file mode 100644 index 00000000000..8218bfd8547 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java @@ -0,0 +1,55 @@ +package org.prebid.server.bidadjustments; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; + +public class BidAdjustmentFactorResolver { + + public BigDecimal resolve(ImpMediaType impMediaType, + ExtRequestBidAdjustmentFactors adjustmentFactors, + String bidder, + String seat) { + + final EnumMap> adjustmentFactorsByMediaTypes = + adjustmentFactors.getMediatypes(); + final Map adjustmentsFactors = adjustmentFactors.getAdjustments(); + + return resolveFromMediaTypes(impMediaType, seat, adjustmentFactorsByMediaTypes) + .or(() -> resolveFromAdjustments(seat, adjustmentsFactors)) + .or(() -> resolveFromMediaTypes(impMediaType, bidder, adjustmentFactorsByMediaTypes)) + .or(() -> resolveFromAdjustments(bidder, adjustmentsFactors)) + .orElse(BigDecimal.ONE); + } + + private static Optional resolveFromMediaTypes( + ImpMediaType mediaType, + String bidderCode, + EnumMap> adjustmentFactors) { + + if (MapUtils.isEmpty(adjustmentFactors)) { + return Optional.empty(); + } + + return Optional.ofNullable(mediaType) + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) + .map(adjustmentFactors::get) + .flatMap(factors -> factors.entrySet().stream() + .filter(entry -> StringUtils.equalsIgnoreCase(entry.getKey(), bidderCode)) + .map(Map.Entry::getValue) + .findFirst()); + } + + private static Optional resolveFromAdjustments(String bidderCode, + Map adjustmentFactors) { + + return Optional.ofNullable(adjustmentFactors) + .map(factors -> factors.get(bidderCode)); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java new file mode 100644 index 00000000000..34495d2cea3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java @@ -0,0 +1,100 @@ +package org.prebid.server.bidadjustments; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.validation.ValidationException; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BidAdjustmentRulesValidator { + + public static final Set SUPPORTED_MEDIA_TYPES = Set.of( + BidAdjustmentsRulesResolver.WILDCARD, + ImpMediaType.banner.toString(), + ImpMediaType.audio.toString(), + ImpMediaType.video_instream.toString(), + ImpMediaType.video_outstream.toString(), + ImpMediaType.xNative.toString()); + + private BidAdjustmentRulesValidator() { + + } + + public static void validate(BidAdjustments bidAdjustments) throws ValidationException { + if (bidAdjustments == null) { + return; + } + + final Map>>> mediatypes = + bidAdjustments.getRules(); + + if (MapUtils.isEmpty(mediatypes)) { + return; + } + + for (String mediatype : mediatypes.keySet()) { + if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + if (MapUtils.isEmpty(bidders)) { + throw new ValidationException("no bidders found in %s".formatted(mediatype)); + } + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + + if (MapUtils.isEmpty(deals)) { + throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder)); + } + + for (String dealId : deals.keySet()) { + final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId); + validateRules(deals.get(dealId), path); + } + } + } + } + } + + private static void validateRules(List rules, + String path) throws ValidationException { + + if (rules == null) { + throw new ValidationException("no bid adjustment rules found in %s".formatted(path)); + } + + for (BidAdjustmentsRule rule : rules) { + final BidAdjustmentType type = rule.getAdjType(); + final String currency = rule.getCurrency(); + final BigDecimal value = rule.getValue(); + + final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency); + + final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN; + + final boolean invalidCpm = type == BidAdjustmentType.CPM + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER + && isValueNotInRange(value, 0, 100); + + final boolean invalidStatic = type == BidAdjustmentType.STATIC + && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE)); + + if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) { + throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path)); + } + } + } + + private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) { + return value == null + || value.compareTo(BigDecimal.valueOf(minValue)) < 0 + || value.compareTo(BigDecimal.valueOf(maxValue)) >= 0; + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java new file mode 100644 index 00000000000..7ac48388c2e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java @@ -0,0 +1,105 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.auction.model.AuctionContext; +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 org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.validation.ValidationException; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsEnricher { + + private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsEnricher.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final ObjectMapper mapper; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + private final double samplingRate; + + public BidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger, double samplingRate) { + this.jacksonMapper = Objects.requireNonNull(mapper); + this.mapper = mapper.mapper(); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.samplingRate = samplingRate; + } + + public BidRequest enrichBidRequest(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List debugWarnings = auctionContext.getDebugWarnings(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + final JsonNode requestNode = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode accountNode = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidAdjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode mergedNode = jsonMerger.merge(requestNode, accountNode); + + final List resolvedWarnings = debugEnabled ? debugWarnings : null; + final JsonNode resolvedBidAdjustments = convertAndValidate(mergedNode, resolvedWarnings, "request") + .or(() -> convertAndValidate(accountNode, resolvedWarnings, "account")) + .orElse(null); + + return bidRequest.toBuilder() + .ext(updateExtRequestWithBidAdjustments(bidRequest, resolvedBidAdjustments)) + .build(); + } + + private Optional convertAndValidate(JsonNode bidAdjustmentsNode, + List debugWarnings, + String errorLocation) { + + if (bidAdjustmentsNode.isEmpty()) { + return Optional.empty(); + } + + try { + final BidAdjustments bidAdjustments = mapper.convertValue(bidAdjustmentsNode, BidAdjustments.class); + + BidAdjustmentRulesValidator.validate(bidAdjustments); + return Optional.of(bidAdjustmentsNode); + } catch (IllegalArgumentException | ValidationException e) { + final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); + if (debugWarnings != null) { + debugWarnings.add(message); + } + conditionalLogger.error(message, samplingRate); + return Optional.empty(); + } + } + + private ExtRequest updateExtRequestWithBidAdjustments(BidRequest bidRequest, JsonNode bidAdjustments) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid updatedPrebid = Optional.ofNullable(extRequest) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::toBuilder) + .orElse(ExtRequestPrebid.builder()) + .bidadjustments((ObjectNode) bidAdjustments) + .build(); + + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + return extRequest == null + ? updatedExtRequest + : jacksonMapper.fillExtension(updatedExtRequest, extRequest.getProperties()); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java new file mode 100644 index 00000000000..c161dcce193 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -0,0 +1,227 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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.ImpMediaTypeResolver; +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.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +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.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.util.PbsUtil; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class BidAdjustmentsProcessor { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + private static final String PREBID_EXT = "prebid"; + + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidAdjustmentsResolver bidAdjustmentsResolver; + private final ObjectMapper mapper; + + public BidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver); + this.mapper = Objects.requireNonNull(mapper).mapper(); + } + + public AuctionParticipation enrichWithAdjustedBids(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 errors = new ArrayList<>(seatBid.getErrors()); + final String bidder = auctionParticipation.getBidder(); + + final List updatedBidderBids = bidderBids.stream() + .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + final BidderResponse updatedBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + + return auctionParticipation.with(updatedBidderResponse); + } + + private BidderBid applyBidAdjustments(BidderBid bidderBid, + BidRequest bidRequest, + String bidder, + List errors) { + try { + final Price originalPrice = getOriginalPrice(bidderBid); + + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), + bidRequest.getImp(), + bidderBid.getType()); + + final Price priceWithFactorsApplied = applyBidAdjustmentFactors( + originalPrice, + getAdapterCode(bidderBid.getBid()), + bidderBid.getSeat(), + bidRequest, + mediaType); + + final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( + priceWithFactorsApplied, + bidderBid.getSeat(), + bidder, + bidRequest, + mediaType, + bidderBid.getBid().getDealid()); + + return updateBid(originalPrice, priceWithAdjustmentsApplied, bidderBid, bidRequest); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + return null; + } + } + + private String getAdapterCode(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .filter(ext -> ext.hasNonNull(PREBID_EXT)) + .map(this::convertValue) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::getAdapterCode) + .orElse(null); + } + + private ExtBidPrebid convertValue(JsonNode jsonNode) { + try { + return mapper.convertValue(jsonNode.get(PREBID_EXT), ExtBidPrebid.class); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) { + final Bid bid = bidderBid.getBid(); + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.createObjectNode(); + + final BigDecimal originalBidPrice = originalPrice.getValue(); + final String originalBidCurrency = originalPrice.getCurrency(); + updatedBidExt.set(ORIGINAL_BID_CPM, new DecimalNode(originalBidPrice)); + if (StringUtils.isNotBlank(originalBidCurrency)) { + updatedBidExt.set(ORIGINAL_BID_CURRENCY, new TextNode(originalBidCurrency)); + } + + final String requestCurrency = bidRequest.getCur().getFirst(); + final BigDecimal requestCurrencyPrice = currencyService.convertCurrency( + adjustedPrice.getValue(), + bidRequest, + adjustedPrice.getCurrency(), + requestCurrency); + + return bidderBid.toBuilder() + .bidCurrency(requestCurrency) + .bid(bid.toBuilder() + .ext(updatedBidExt) + .price(requestCurrencyPrice) + .build()) + .build(); + } + + private Price getOriginalPrice(BidderBid bidderBid) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + return Price.of(StringUtils.stripToNull(bidCurrency), price); + } + + private Price applyBidAdjustmentFactors(Price bidPrice, + String bidder, + String seat, + BidRequest bidRequest, + ImpMediaType mediaType) { + + final String bidCurrency = bidPrice.getCurrency(); + final BigDecimal price = bidPrice.getValue(); + + final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, seat, bidRequest, mediaType); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, price); + + return Price.of(bidCurrency, adjustedPrice.compareTo(price) != 0 ? adjustedPrice : price); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, + String seat, + BidRequest bidRequest, + ImpMediaType mediaType) { + + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + return adjustmentFactors == null + ? null + : bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder, seat); + } + + 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 Price applyBidAdjustmentRules(Price bidPrice, + String seat, + String bidder, + BidRequest bidRequest, + ImpMediaType mediaType, + String dealId) { + + return bidAdjustmentsResolver.resolve( + bidPrice, + bidRequest, + mediaType, + seat, + bidder, + dealId); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java new file mode 100644 index 00000000000..46947c40e67 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -0,0 +1,68 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; + +public class BidAdjustmentsResolver { + + private final CurrencyConversionService currencyService; + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + + public BidAdjustmentsResolver(CurrencyConversionService currencyService, + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver) { + + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); + } + + public Price resolve(Price initialPrice, + BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetSeat, + String targetBidder, + String targetDealId) { + + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetSeat, targetBidder, targetDealId); + + return adjustPrice(initialPrice, rules, bidRequest); + } + + private Price adjustPrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (BidAdjustmentsRule rule : bidAdjustmentRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = BidderUtil.roundFloor(resolvedPrice.multiply(adjustmentValue)); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.subtract(convertedAdjustmentValue)); + } + case STATIC -> { + resolvedPrice = adjustmentValue; + resolvedCurrency = adjustmentCurrency; + } + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java new file mode 100644 index 00000000000..d133d997520 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java @@ -0,0 +1,94 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; +import org.prebid.server.json.JacksonMapper; +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.ImpMediaType; +import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; +import org.prebid.server.util.dsl.config.PrebidConfigParameter; +import org.prebid.server.util.dsl.config.PrebidConfigParameters; +import org.prebid.server.util.dsl.config.PrebidConfigSource; +import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; +import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; +import org.prebid.server.util.dsl.config.impl.SimpleParameters; +import org.prebid.server.util.dsl.config.impl.SimpleSource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsRulesResolver { + + public static final String WILDCARD = "*"; + public static final String DELIMITER = "|"; + + private final PrebidConfigMatchingStrategy matchingStrategy; + private final ObjectMapper mapper; + + public BidAdjustmentsRulesResolver(JacksonMapper mapper) { + this.matchingStrategy = new MostAccurateCombinationStrategy(); + this.mapper = Objects.requireNonNull(mapper).mapper(); + } + + public List resolve(BidRequest bidRequest, ImpMediaType targetMediaType, String targetBidder) { + return resolve(bidRequest, targetMediaType, null, targetBidder, null); + } + + public List resolve(BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetSeat, + String targetBidder, + String targetDealId) { + + final BidAdjustmentsRules bidAdjustments = BidAdjustmentsRules.of(extractBidAdjustments(bidRequest)); + return findRules(bidAdjustments, targetMediaType, targetSeat, targetBidder, targetDealId); + } + + private BidAdjustments extractBidAdjustments(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .map(node -> mapper.convertValue(node, BidAdjustments.class)) + .orElse(null); + } + + private List findRules(BidAdjustmentsRules bidAdjustments, + ImpMediaType targetMediaType, + String targetSeat, + String targetBidder, + String targetDealId) { + + final Map> rules = bidAdjustments.getRules(); + final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); + final PrebidConfigParameters parameters = createParameters( + targetMediaType, targetSeat, targetBidder, targetDealId); + + final String rule = matchingStrategy.match(source, parameters); + return rule == null ? Collections.emptyList() : rules.get(rule); + } + + private PrebidConfigParameters createParameters(ImpMediaType mediaType, + String seat, + String bidder, + String dealId) { + + final List conditionsMatchers = List.of( + SimpleDirectParameter.of(mediaType.toString()), + StringUtils.isBlank(seat) + ? SimpleDirectParameter.of(bidder) + : SimpleDirectParameter.of(List.of(seat, bidder)), + StringUtils.isBlank(dealId) + ? PrebidConfigParameter.wildcard() + : SimpleDirectParameter.of(dealId)); + + return SimpleParameters.of(conditionsMatchers); + } +} diff --git a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java similarity index 82% rename from src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java rename to src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java index 7a42ef8e464..86dd863d1c3 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -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) { @@ -45,6 +32,7 @@ public BigDecimal resolve(Set impMediaTypes, } final BigDecimal mediaTypeMinFactor = impMediaTypes.stream() + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) .map(adjustmentFactorsByMediaTypes::get) .map(bidderToFactor -> MapUtils.isNotEmpty(bidderToFactor) ? bidderToFactor.entrySet().stream() diff --git a/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java new file mode 100644 index 00000000000..cb4485bfa35 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java @@ -0,0 +1,89 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class FloorAdjustmentsResolver { + + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + private final CurrencyConversionService currencyService; + + public FloorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); + this.currencyService = Objects.requireNonNull(currencyService); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + Set targetMediaTypes, + String targetBidder) { + + final String currency = bidRequest.getCur().getFirst(); + Price minimalBidFloorPrice = null; + BigDecimal minimalPriceBidFloorValue = new BigDecimal(Integer.MAX_VALUE); + + for (ImpMediaType targetMediaType : targetMediaTypes) { + final Price resolvedPrice = resolve(initialBidFloorPrice, bidRequest, targetMediaType, targetBidder); + final BigDecimal convertedResolvedValue = currencyService.convertCurrency( + resolvedPrice.getValue(), bidRequest, resolvedPrice.getCurrency(), currency); + if (convertedResolvedValue.compareTo(minimalPriceBidFloorValue) < 0) { + minimalBidFloorPrice = resolvedPrice; + minimalPriceBidFloorValue = convertedResolvedValue; + } + } + + return ObjectUtils.firstNonNull(minimalBidFloorPrice, initialBidFloorPrice); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetBidder) { + + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetBidder); + return reversePrice(initialBidFloorPrice, rules, bidRequest); + } + + private Price reversePrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + final List reversedRules = bidAdjustmentRules.reversed(); + final String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (BidAdjustmentsRule rule : reversedRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = resolvedPrice.divide(adjustmentValue, 4, RoundingMode.HALF_EVEN); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.add(convertedAdjustmentValue)); + } + case STATIC -> throw new PreBidException("STATIC type can't be applied to a floor price"); + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java new file mode 100644 index 00000000000..e9b790e5eab --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java @@ -0,0 +1,19 @@ +package org.prebid.server.bidadjustments.model; + +import com.fasterxml.jackson.annotation.JsonCreator; + +public enum BidAdjustmentType { + + CPM, MULTIPLIER, STATIC, UNKNOWN; + + @SuppressWarnings("unused") + @JsonCreator + public static BidAdjustmentType of(String name) { + try { + return BidAdjustmentType.valueOf(name.toUpperCase()); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java new file mode 100644 index 00000000000..c57f5e7b26a --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidadjustments.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustments { + + @JsonProperty("mediatype") + Map>>> rules; + +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java new file mode 100644 index 00000000000..dec501d71b2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidadjustments.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder(toBuilder = true) +@Value +public class BidAdjustmentsRule { + + @JsonProperty("adjtype") + BidAdjustmentType adjType; + + BigDecimal value; + + String currency; + + public String toString() { + return "[adjtype=%s, value=%s, currency=%s]".formatted(adjType, value, currency); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java new file mode 100644 index 00000000000..6d64ca1330e --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java @@ -0,0 +1,50 @@ +package org.prebid.server.bidadjustments.model; + +import lombok.Value; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.collections4.map.CaseInsensitiveMap; +import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustmentsRules { + + private static final String RULE_SCHEME = + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s"; + + Map> rules; + + public static BidAdjustmentsRules of(BidAdjustments bidAdjustments) { + if (bidAdjustments == null) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + final Map> rules = new CaseInsensitiveMap<>(); + + final Map>>> mediatypes = + bidAdjustments.getRules(); + + if (MapUtils.isEmpty(mediatypes)) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + for (String mediatype : mediatypes.keySet()) { + if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + for (String dealId : deals.keySet()) { + rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); + } + } + } + } + + return BidAdjustmentsRules.of(MapUtils.unmodifiableMap(rules)); + } + +} diff --git a/src/main/java/org/prebid/server/bidder/BidderCatalog.java b/src/main/java/org/prebid/server/bidder/BidderCatalog.java index 3d8db57e941..d8eb543780b 100644 --- a/src/main/java/org/prebid/server/bidder/BidderCatalog.java +++ b/src/main/java/org/prebid/server/bidder/BidderCatalog.java @@ -136,7 +136,7 @@ public boolean isDebugAllowed(String name) { /** * Returns an {@link BidderInfo} registered by the given name or null if there is none. *

- * Therefore this method should be called only for names that previously passed validity check + * Therefore, this method should be called only for names that previously passed validity check * through calling {@link #isValidName(String)}. */ public BidderInfo bidderInfoByName(String name) { @@ -149,7 +149,7 @@ public BidderInfo bidderInfoByName(String name) { /** * Returns an VendorId registered by the given name or null if there is none. *

- * Therefore this method should be called only for names that previously passed validity check + * Therefore, this method should be called only for names that previously passed validity check * through calling {@link #isValidName(String)}. */ public Integer vendorIdByName(String name) { @@ -217,7 +217,7 @@ public Set usersyncReadyBidders() { /** * Returns an {@link Bidder} registered by the given name or null if there is none. *

- * Therefore this method should be called only for names that previously passed validity check + * Therefore, this method should be called only for names that previously passed validity check * through calling {@link #isValidName(String)}. */ public Bidder bidderByName(String name) { @@ -226,4 +226,11 @@ public Bidder bidderByName(String name) { .map(BidderInstanceDeps::getBidder) .orElse(null); } + + public String configuredName(String name) { + return Optional.ofNullable(name) + .map(bidderDepsMap::get) + .map(BidderInstanceDeps::getName) + .orElse(null); + } } 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..d26cc2c4b2c 100644 --- a/src/main/java/org/prebid/server/bidder/BidderInfo.java +++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java @@ -7,7 +7,9 @@ import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.MediaType; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Value(staticConstructor = "of") public class BidderInfo { @@ -28,6 +30,8 @@ public class BidderInfo { List vendors; + Set currencyAccepted; + GdprInfo gdpr; boolean ccpaEnforced; @@ -38,6 +42,8 @@ public class BidderInfo { Ortb ortb; + long tmaxDeductionMs; + public static BidderInfo create(boolean enabled, OrtbVersion ortbVersion, boolean debugAllowed, @@ -49,10 +55,12 @@ public static BidderInfo create(boolean enabled, List doohMediaTypes, List supportedVendors, int vendorId, + List currencyAccepted, boolean ccpaEnforced, boolean modifyingVastXmlAllowed, CompressionType compressionType, - org.prebid.server.spring.config.bidder.model.Ortb ortb) { + org.prebid.server.spring.config.bidder.model.Ortb ortb, + long tmaxDeductionMs) { return of( enabled, @@ -66,11 +74,13 @@ public static BidderInfo create(boolean enabled, platformInfo(siteMediaTypes), platformInfo(doohMediaTypes)), supportedVendors, + currencyAccepted != null ? new HashSet<>(currencyAccepted) : null, new GdprInfo(vendorId), ccpaEnforced, modifyingVastXmlAllowed, compressionType, - Ortb.of(ortb.getMultiFormatSupported())); + Ortb.of(ortb.getMultiFormatSupported()), + tmaxDeductionMs); } private static PlatformInfo platformInfo(List mediaTypes) { 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/HttpBidderRequestEnricher.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java index 4aabd5960a5..3e37eb685ca 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequestEnricher.java @@ -5,7 +5,7 @@ import io.netty.handler.codec.http.HttpHeaderValues; import io.vertx.core.MultiMap; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; @@ -48,7 +48,7 @@ MultiMap enrichHeaders( BidderAliases aliases, BidRequest bidRequest) { - // some bidders has headers on class level, so we create copy to not affect them + // some bidders have headers on class level, so we create copy to not affect them final MultiMap bidderRequestHeadersCopy = copyMultiMap(bidderRequestHeaders); addOriginalRequestHeaders(bidderRequestHeadersCopy, originalRequestHeaders); diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java index 42976d52256..61e074ecb26 100644 --- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java +++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java @@ -4,14 +4,11 @@ import io.netty.channel.ConnectTimeoutException; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; -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; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.model.BidRejectionReason; import org.prebid.server.auction.model.BidRejectionTracker; @@ -26,14 +23,18 @@ import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; 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.model.CaseInsensitiveMultiMap; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; 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; @@ -62,24 +63,28 @@ public class HttpBidderRequester { private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private final HttpClient httpClient; private final BidderRequestCompletionTrackerFactory completionTrackerFactory; private final BidderErrorNotifier bidderErrorNotifier; private final HttpBidderRequestEnricher requestEnricher; private final JacksonMapper mapper; + private final double logSamplingRate; public HttpBidderRequester(HttpClient httpClient, BidderRequestCompletionTrackerFactory completionTrackerFactory, BidderErrorNotifier bidderErrorNotifier, HttpBidderRequestEnricher requestEnricher, - JacksonMapper mapper) { + JacksonMapper mapper, + double logSamplingRate) { this.httpClient = Objects.requireNonNull(httpClient); this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory); this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier); this.requestEnricher = Objects.requireNonNull(requestEnricher); this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; } /** @@ -100,7 +105,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 +116,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 @@ -124,8 +130,8 @@ public Future requestBids(Bidder bidder, .map(httpCall -> processHttpCall(bidder, bidRequest, resultBuilder, httpCall))) .toList(); - return CompositeFuture.any( - CompositeFuture.join(new ArrayList<>(httpRequestFutures)), + return Future.any( + Future.join(httpRequestFutures), completionTracker.future()) .map(ignored -> resultBuilder.toBidderSeatBid(debugEnabled)) .onSuccess(seatBid -> bidRejectionTracker.restoreFromRejection(seatBid.getBids())); @@ -144,11 +150,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.rejectImps(error.getImpIds(), reason)); } private boolean isStoredResponse(List> httpRequests, String storedResponse, String bidder) { @@ -159,7 +167,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; } @@ -238,10 +246,10 @@ 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}", - httpRequest.getUri(), exception.getMessage()); - logger.debug("Error occurred while sending HTTP request to a bidder url: {0}", + private Future> failResponse(Throwable exception, HttpRequest httpRequest) { + conditionalLogger.warn("Error occurred while sending HTTP request to a bidder url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); + logger.debug("Error occurred while sending HTTP request to a bidder url: {}", exception, httpRequest.getUri()); final BidderError.Type errorType = @@ -313,7 +321,7 @@ private static CompositeBidderResponse makeBids(Bidder bidder, /** * Replaces body of {@link HttpResponse} with empty JSON object if response HTTP status code is equal to 204. *

- * Note: this will safe making bids by bidders from JSON parsing error. + * Note: this will save making bids by bidders from JSON parsing error. */ private static BidderCall toHttpCallWithSafeResponseBody(BidderCall httpCall) { final HttpResponse response = httpCall.getResponse(); @@ -338,6 +346,7 @@ private static class ResultBuilder { private final Map, BidderCall> bidderCallsRecorded = new HashMap<>(); private final List bidsRecorded = new ArrayList<>(); private final List errorsRecorded = new ArrayList<>(); + private final List igiRecorded = new ArrayList<>(); private final List fledgeRecorded = new ArrayList<>(); ResultBuilder(List> httpRequests, @@ -358,6 +367,7 @@ void addHttpCall(BidderCall bidderCall, CompositeBidderResponse bidderRespons handleBids(bidderResponse); handleBidderErrors(bidderResponse); handleBidderCallError(bidderCall); + handleIgis(bidderResponse); handleFledgeAuctionConfigs(bidderResponse); } @@ -374,17 +384,45 @@ 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.rejectImps(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE); + return; + } + + if (statusCode != null + && (statusCode < HttpResponseStatus.OK.code() + || statusCode >= HttpResponseStatus.BAD_REQUEST.code())) { + + bidRejectionTracker.rejectImps(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; } + + final BidRejectionReason reason = callErrorType == BidderError.Type.timeout + ? BidRejectionReason.ERROR_TIMED_OUT + : BidRejectionReason.ERROR_GENERAL; + + bidRejectionTracker.rejectImps(requestedImpIds, reason); } private void handleFledgeAuctionConfigs(CompositeBidderResponse bidderResponse) { @@ -393,6 +431,12 @@ private void handleFledgeAuctionConfigs(CompositeBidderResponse bidderResponse) .ifPresent(fledgeRecorded::addAll); } + private void handleIgis(CompositeBidderResponse bidderResponse) { + Optional.ofNullable(bidderResponse) + .map(CompositeBidderResponse::getIgi) + .ifPresent(igiRecorded::addAll); + } + BidderSeatBid toBidderSeatBid(boolean debugEnabled) { final List> httpCalls = new ArrayList<>(bidderCallsRecorded.values()); httpRequests.stream() @@ -410,6 +454,7 @@ BidderSeatBid toBidderSeatBid(boolean debugEnabled) { .bids(bidsRecorded) .httpCalls(extHttpCalls) .errors(errors) + .igi(igiRecorded) .fledgeAuctionConfigs(fledgeRecorded) .build(); } diff --git a/src/main/java/org/prebid/server/bidder/Usersyncer.java b/src/main/java/org/prebid/server/bidder/Usersyncer.java index e5b6cad6f6e..ebe776d2e3e 100644 --- a/src/main/java/org/prebid/server/bidder/Usersyncer.java +++ b/src/main/java/org/prebid/server/bidder/Usersyncer.java @@ -3,6 +3,8 @@ import lombok.Value; import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; +import java.util.List; + @Value(staticConstructor = "of") public class Usersyncer { @@ -16,7 +18,23 @@ public class Usersyncer { UsersyncMethod redirect; - public static Usersyncer of(String cookieFamilyName, UsersyncMethod iframe, UsersyncMethod redirect) { - return of(true, cookieFamilyName, CookieFamilySource.ROOT, iframe, redirect); + boolean skipWhenInGdprScope; + + List gppSidToSkip; + + public static Usersyncer of(String cookieFamilyName, + UsersyncMethod iframe, + UsersyncMethod redirect, + boolean skipWhenInGdprScope, + List gppSidToSkip) { + + return of( + true, + cookieFamilyName, + CookieFamilySource.ROOT, + iframe, + redirect, + skipWhenInGdprScope, + gppSidToSkip); } } 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/adagio/AdagioBidder.java b/src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java new file mode 100644 index 00000000000..a211f6267b4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adagio/AdagioBidder.java @@ -0,0 +1,100 @@ +package org.prebid.server.bidder.adagio; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +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.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class AdagioBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AdagioBidder(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 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())) { + errors.add(BidderError.badServerResponse("empty seatbid array")); + return Collections.emptyList(); + } + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .flatMap(seatBid -> seatBid.getBid().stream() + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors))) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + try { + final ExtBidPrebid extBidPrebid = parseBidExt(bid); + return BidderBid.builder() + .bid(bid) + .type(getBidType(bid)) + .bidCurrency(currency) + .videoInfo(extBidPrebid != null ? extBidPrebid.getVideo() : null) + .build(); + + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private ExtBidPrebid parseBidExt(Bid bid) { + try { + return mapper.mapper().convertValue(bid.getExt(), ExtBidPrebid.class); + } catch (IllegalArgumentException e) { + throw new PreBidException("bid.ext can not be parsed"); + } + } + + 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( + "Could not define media type for impression: " + bid.getImpid()); + }; + } +} 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/adgeneration/model/AdgenerationResponse.java b/src/main/java/org/prebid/server/bidder/adgeneration/model/AdgenerationResponse.java index 1d56b5e8bcb..d8c6d0b615c 100644 --- a/src/main/java/org/prebid/server/bidder/adgeneration/model/AdgenerationResponse.java +++ b/src/main/java/org/prebid/server/bidder/adgeneration/model/AdgenerationResponse.java @@ -2,14 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdgenerationResponse { String locationid; 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..4c72fd97760 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(); @@ -127,7 +127,7 @@ public Result> makeBids(BidderCall httpCall, BidRequ } final Bid bid = optionalBid.get(); - final AdheseOriginData originData = toObjectOfType(bid.getExt().get("adhese"), AdheseOriginData.class); + final AdheseOriginData originData = toAdheseOriginData(bid.getExt().get("adhese")); final Bid modifiedBid = bid.toBuilder() .ext(mapper.mapper().valueToTree(originData)) // unwrap from "adhese" .build(); @@ -151,9 +151,9 @@ private static Optional getBid(BidResponse bidResponse) { .findFirst(); } - private T toObjectOfType(JsonNode jsonNode, Class clazz) { + private AdheseOriginData toAdheseOriginData(JsonNode jsonNode) { try { - return mapper.mapper().treeToValue(jsonNode, clazz); + return mapper.mapper().treeToValue(jsonNode, AdheseOriginData.class); } catch (JsonProcessingException e) { throw new PreBidException(e.getMessage(), e); } @@ -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/adhese/model/AdheseOriginData.java b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseOriginData.java index 2719b6d8f54..10ad9caac4a 100644 --- a/src/main/java/org/prebid/server/bidder/adhese/model/AdheseOriginData.java +++ b/src/main/java/org/prebid/server/bidder/adhese/model/AdheseOriginData.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.adhese.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AdheseOriginData { String priority; 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..c50b89b3417 100644 --- a/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java +++ b/src/main/java/org/prebid/server/bidder/adkernel/AdkernelBidder.java @@ -218,39 +218,35 @@ private static BidRequest createBidRequest(List imps, 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 | PreBidException 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 || bidResponse.getSeatbid() == null) { return Collections.emptyList(); } if (bidResponse.getSeatbid().size() != 1) { throw new PreBidException("Invalid SeatBids count: " + bidResponse.getSeatbid().size()); } - 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() .map(SeatBid::getBid) .flatMap(Collection::stream) - .map(bid -> makeBidderBid(bid, bidRequest.getImp(), bidResponse.getCur())) + .map(bid -> makeBidderBid(bid, bidResponse.getCur())) .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency) { + private static BidderBid makeBidderBid(Bid bid, 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..08ed6c64903 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/admatic/AdmaticBidder.java @@ -0,0 +1,143 @@ +package org.prebid.server.bidder.admatic; + +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.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, + headers(modifiedBidRequest.getDevice()), + 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())); + } + + private MultiMap headers(Device device) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + } + + return headers; + } + + @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..127d02ae000 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/AdnuntiusBidder.java @@ -1,33 +1,41 @@ package org.prebid.server.bidder.adnuntius; +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.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; -import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusAdUnit; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusMetaData; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusNativeRequest; import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequest; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusRequestAdUnit; 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.AdnuntiusAdUnit; +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; @@ -35,13 +43,19 @@ import org.prebid.server.bidder.model.Result; 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.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.request.adnuntius.ExtImpAdnuntiusTargeting; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -56,8 +70,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.stream.IntStream; -import java.util.stream.Stream; public class AdnuntiusBidder implements Bidder { @@ -65,86 +77,62 @@ public class AdnuntiusBidder implements Bidder { new TypeReference<>() { }; private static final int SECONDS_IN_MINUTE = 60; - 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 static final int BANNER_MTYPE = 1; + private static final int NATIVE_MTYPE = 4; private final String endpointUrl; + private final String euEndpoint; private final Clock clock; private final JacksonMapper mapper; - public AdnuntiusBidder(String endpointUrl, Clock clock, JacksonMapper mapper) { + public AdnuntiusBidder(String endpointUrl, + String euEndpoint, + Clock clock, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.euEndpoint = euEndpoint == null ? null : HttpUtil.validateUrl(euEndpoint); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { - final Map> networkToAdUnits = new HashMap<>(); - boolean noCookies = false; - - for (Imp imp : request.getImp()) { - final ExtImpAdnuntius extImpAdnuntius; - try { + try { + final Map> networkToAdUnits = new HashMap<>(); + boolean noCookies = false; + for (Imp imp : request.getImp()) { validateImp(imp); - extImpAdnuntius = parseImpExt(imp); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - - noCookies = resolveIsNoCookies(extImpAdnuntius); - final String network = resolveNetwork(extImpAdnuntius); - - networkToAdUnits.computeIfAbsent(network, n -> new ArrayList<>()) - .add(makeAdnuntiusAdUnit(imp, extImpAdnuntius)); - } - - return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); - } + final ExtImpAdnuntius extImpAdnuntius = parseImpExt(imp); + noCookies = noCookies || resolveIsNoCookies(extImpAdnuntius); + final String network = resolveNetwork(extImpAdnuntius); - 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(); - } + final List adUnits = networkToAdUnits.computeIfAbsent( + network, + ignored -> new ArrayList<>()); - private static List> createDimensions(Imp imp) { - final Banner banner = imp.getBanner(); + if (imp.getBanner() != null) { + adUnits.add(makeBannerAdUnit(imp, extImpAdnuntius)); + } - 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())); + if (imp.getXNative() != null) { + adUnits.add(makeNativeAdUnit(imp, extImpAdnuntius)); } } - return formats; - } - if (banner.getW() != null && banner.getH() != null) { - return Collections.singletonList(List.of(banner.getW(), banner.getH())); + return Result.withValues(createHttpRequests(networkToAdUnits, request, noCookies)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } - - 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())); + if (imp.getBanner() == null && imp.getXNative() == null) { + throw new PreBidException("ignoring imp id=%s: Adnuntius supports only native and banner" + .formatted(imp.getId())); } } @@ -159,63 +147,147 @@ 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 AdnuntiusRequestAdUnit makeBannerAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + return makeAdUnitBuilder(imp, extImpAdnuntius, "banner") + .dimensions(createDimensions(imp.getBanner())) + .build(); + } - final List> adnuntiusRequests = new ArrayList<>(); + private static String targetId(String auId, String impId, String bidType) { + return "%s-%s:%s".formatted(auId, impId, bidType); + } - 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)); + } } - return adnuntiusRequests; + if (!formats.isEmpty()) { + return formats; + } + + 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 AdnuntiusRequestAdUnit makeNativeAdUnit(Imp imp, ExtImpAdnuntius extImpAdnuntius) { + return makeAdUnitBuilder(imp, extImpAdnuntius, "native") + .nativeRequest(AdnuntiusNativeRequest.of(parseNativeRequest(imp))) + .adType("NATIVE") + .build(); } - private static String extractPage(Site site) { - return StringUtils.defaultIfBlank(ObjectUtil.getIfNotNull(site, Site::getPage), DEFAULT_PAGE); + private ObjectNode parseNativeRequest(Imp imp) { + try { + return mapper.mapper().readValue(imp.getXNative().getRequest(), ObjectNode.class); + } catch (IllegalArgumentException | JsonProcessingException e) { + throw new PreBidException("Unmarshalling Native error " + e.getMessage()); + } } - private String createUri(BidRequest bidRequest, Boolean noCookies) { + private static AdnuntiusRequestAdUnit.AdnuntiusRequestAdUnitBuilder makeAdUnitBuilder( + Imp imp, + ExtImpAdnuntius extImpAdnuntius, + String bidType) { + + final String auId = extImpAdnuntius.getAuId(); + final ExtImpAdnuntiusTargeting targeting = ObjectUtils.defaultIfNull( + extImpAdnuntius.getTargeting(), + ExtImpAdnuntiusTargeting.builder().build()); + return AdnuntiusRequestAdUnit.builder() + .auId(auId) + .targetId(targetId(auId, imp.getId(), bidType)) + .maxDeals(resolveMaxDeals(extImpAdnuntius)) + .category(targeting.getCategory()) + .segments(targeting.getSegments()) + .keywords(targeting.getKeywords()) + .keyValues(targeting.getKeyValues()) + .adUnitMatchingLabel(targeting.getAdUnitMatchingLabel()); + } + + private static Integer resolveMaxDeals(ExtImpAdnuntius extImpAdnuntius) { + final Integer maxDeals = extImpAdnuntius.getMaxDeals(); + return maxDeals != null && maxDeals > 0 ? maxDeals : null; + } + + private List> createHttpRequests( + Map> networkToAdUnits, + BidRequest request, + boolean noCookies) { + + final Site site = request.getSite(); + + final String uri = makeEndpoint(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(request, adnuntiusRequest, uri, request.getDevice())); + } + + return adnuntiusRequests; + } + + private String makeEndpoint(BidRequest bidRequest, Boolean noCookies) { try { - final URIBuilder uriBuilder = new URIBuilder(endpointUrl) - .addParameter("format", "json") + final String gdpr = extractGdpr(bidRequest.getRegs()); + final String url = StringUtils.isNotBlank(gdpr) ? euEndpoint : endpointUrl; + + if (url == null) { + throw new PreBidException("an EU endpoint is required but invalid"); + } + + final URIBuilder uriBuilder = new URIBuilder(url) + .addParameter("format", "prebidServer") .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(); - } catch (URISyntaxException e) { + } catch (URISyntaxException | IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } } @@ -225,36 +297,78 @@ 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(BidRequest request, + 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) + .impIds(BidderUtil.impIds(request)) .build(); } - private MultiMap getHeaders(Device device) { + private MultiMap headers(Device device) { final MultiMap headers = HttpUtil.headers(); if (device != null) { @@ -271,90 +385,144 @@ public Result> makeBids(BidderCall httpCall, B final String body = httpCall.getResponse().getBody(); final AdnuntiusResponse adnuntiusResponse = mapper.decodeValue(body, AdnuntiusResponse.class); return Result.withValues(extractBids(bidRequest, adnuntiusResponse)); - } catch (DecodeException | PreBidException e) { + } catch (EncodeException | DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } + private static Map parseAdUnits(AdnuntiusResponse adnuntiusResponse) { + final Map targetIdToAdsUnit = new HashMap<>(); + for (AdnuntiusAdUnit adUnit : adnuntiusResponse.getAdUnits()) { + if (isValid(adUnit)) { + final String targetId = extractTargetId(adUnit.getTargetId()); + final AdnuntiusAdUnit existingAdUnit = targetIdToAdsUnit.get(targetId); + if (existingAdUnit == null || getBidAmount(adUnit).compareTo(getBidAmount(existingAdUnit)) >= 0) { + targetIdToAdsUnit.put(targetId, adUnit); + } + } + } + + return targetIdToAdsUnit; + } + private List extractBids(BidRequest bidRequest, AdnuntiusResponse adnuntiusResponse) { - if (adnuntiusResponse == null || CollectionUtils.isEmpty(adnuntiusResponse.getAdsUnits())) { + if (adnuntiusResponse == null || CollectionUtils.isEmpty(adnuntiusResponse.getAdUnits())) { 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 = parseAdUnits(adnuntiusResponse); - 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 = targetIdForBids(extImpAdnuntius.getAuId(), imp.getId()); + + final AdnuntiusAdUnit adUnit = targetIdToAdsUnit.get(targetId); + if (adUnit == null) { + continue; + } + + final AdnuntiusAd ad = adUnit.getAds().getFirst(); + final String impId = imp.getId(); + final String bidType = extImpAdnuntius.getBidType(); - final String currency = extractCurrency(validAdsUnitToImp); - final Stream generalBids = validAdsUnitToImp.stream() - .map(adsUnitWithImpId -> makeGeneralBid(adsUnitWithImpId, currency)); + currency = ObjectUtil.getIfNotNull(ad.getBid(), AdnuntiusBid::getCurrency); + final JsonNode nativeRequest = Optional.ofNullable(adUnit.getNativeJson()) + .map(AdnuntiusNativeRequest::getOrtb) + .orElse(null); + final int mType = nativeRequest == null ? BANNER_MTYPE : NATIVE_MTYPE; + final String html = nativeRequest == null ? adUnit.getHtml() : mapper.encodeToString(nativeRequest); - final Stream dealBids = validAdsUnitToImp.stream() - .filter(adsUnitWithImpId -> CollectionUtils.isNotEmpty(adsUnitWithImpId.getAdsUnit().getDeals())) - .map(adsUnitWithImpId -> makeDealsBid(adsUnitWithImpId, currency)) - .filter(Objects::nonNull); + bids.add(createBid(ad, bidRequest, html, impId, bidType, mType)); - return Stream.concat(generalBids, dealBids).toList(); + for (AdnuntiusAd deal : ListUtils.emptyIfNull(adUnit.getDeals())) { + bids.add(createBid(deal, bidRequest, deal.getHtml(), impId, bidType, BANNER_MTYPE)); + } + } + + final String lastCurrency = currency; + return bids.stream() + .map(bid -> BidderBid.of( + bid, + bid.getMtype() == BANNER_MTYPE ? BidType.banner : BidType.xNative, + lastCurrency)) + .toList(); } - private static boolean validateAdsUnit(AdnuntiusAdsUnit adsUnit) { - final List ads = ObjectUtil.getIfNotNull(adsUnit, AdnuntiusAdsUnit::getAds); - return CollectionUtils.isNotEmpty(ads) && ads.get(0) != null; + private static BigDecimal getBidAmount(AdnuntiusAdUnit adUnit) { + return adUnit.getAds().getFirst().getBid().getAmount(); } - 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); + private static boolean isValid(AdnuntiusAdUnit adsUnit) { + if (adsUnit == null) { + return false; + } + + final String targetId = extractTargetId(adsUnit.getTargetId()); + final int matchedCount = ObjectUtils.defaultIfNull(adsUnit.getMatchedAdCount(), 0); + final List ads = adsUnit.getAds(); + final BigDecimal bidAmount = CollectionUtils.emptyIfNull(ads).stream() + .findFirst() + .map(AdnuntiusAd::getBid) + .map(AdnuntiusBid::getAmount) + .orElse(null); + + return targetId != null && matchedCount > 0 && bidAmount != null; } - 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); + private static String extractTargetId(String targetId) { + return targetId == null ? null : targetId.split(":")[0]; } - 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 String targetIdForBids(String auId, String impId) { + return "%s-%s".formatted(auId, impId); } - 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, int mtype) { 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()) - .adomain(extractDomain(ad.getDestinationUrls())) + .price(resolvePrice(ad, bidType)) + .adm(adm) + .adomain(ad.getAdvertiserDomains()) + .mtype(mtype) + .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,21 +538,12 @@ 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; } - - private static List extractDomain(Map destinationUrls) { - return destinationUrls == null ? Collections.emptyList() : destinationUrls.values().stream() - .filter(Objects::nonNull) - .map(url -> url.split("/")) - .filter(splintedUrl -> splintedUrl.length >= 2) - .map(splintedUrl -> splintedUrl[2].replaceAll("www\\.", "")) - .toList(); - } } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java deleted file mode 100644 index f092822f909..00000000000 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusAdUnit.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.bidder.adnuntius.model.request; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder(toBuilder = true) -@Value -public class AdnuntiusAdUnit { - - @JsonProperty("auId") - String auId; - - @JsonProperty("targetId") - String targetId; - - List> dimensions; - - @JsonProperty("maxDeals") - Integer maxDeals; -} 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/AdnuntiusNativeRequest.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java new file mode 100644 index 00000000000..b76f5394b4d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusNativeRequest.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.adnuntius.model.request; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AdnuntiusNativeRequest { + + ObjectNode ortb; + +} 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..2fde60d0044 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,15 +2,18 @@ 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") - List adUnits; + List adUnits; @JsonProperty("metaData") @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -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/request/AdnuntiusRequestAdUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequestAdUnit.java new file mode 100644 index 00000000000..88a37a2dc3b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/request/AdnuntiusRequestAdUnit.java @@ -0,0 +1,43 @@ +package org.prebid.server.bidder.adnuntius.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Builder(toBuilder = true) +@Value +public class AdnuntiusRequestAdUnit { + + @JsonProperty("auId") + String auId; + + @JsonProperty("targetId") + String targetId; + + List> dimensions; + + @JsonProperty("maxDeals") + Integer maxDeals; + + @JsonProperty("nativeRequest") + AdnuntiusNativeRequest nativeRequest; + + @JsonProperty("adType") + String adType; + + @JsonProperty("c") + List category; + + List segments; + + List keywords; + + @JsonProperty("kv") + Map> keyValues; + + @JsonProperty("auml") + List adUnitMatchingLabel; +} 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..20c4b047653 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 @@ -4,6 +4,7 @@ import lombok.Builder; import lombok.Value; +import java.util.List; import java.util.Map; @Builder @@ -13,10 +14,10 @@ public class AdnuntiusAd { AdnuntiusBid bid; @JsonProperty("netBid") - AdnuntiusNetBid adnuntiusNetBid; + AdnuntiusNetBid netBid; @JsonProperty("grossBid") - AdnuntiusGrossBid adnuntiusGrossBid; + AdnuntiusGrossBid grossBid; @JsonProperty("dealId") String dealId; @@ -36,6 +37,13 @@ public class AdnuntiusAd { @JsonProperty("lineItemId") String lineItemId; + String html; + @JsonProperty("destinationUrls") Map destinationUrls; + + @JsonProperty("advertiserDomains") + List advertiserDomains; + + AdnuntiusAdvertiser advertiser; } diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java new file mode 100644 index 00000000000..7dbd6478335 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdUnit.java @@ -0,0 +1,34 @@ +package org.prebid.server.bidder.adnuntius.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.bidder.adnuntius.model.request.AdnuntiusNativeRequest; + +import java.util.List; + +@Builder +@Value +public class AdnuntiusAdUnit { + + @JsonProperty("auId") + String auId; + + @JsonProperty("targetId") + String targetId; + + String html; + + @JsonProperty("matchedAdCount") + Integer matchedAdCount; + + @JsonProperty("responseId") + String responseId; + + @JsonProperty("nativeJson") + AdnuntiusNativeRequest nativeJson; + + List ads; + + List deals; +} diff --git a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java deleted file mode 100644 index 8fb0c0c7c70..00000000000 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusAdsUnit.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.prebid.server.bidder.adnuntius.model.response; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Builder; -import lombok.Value; - -import java.util.List; - -@Builder -@Value -public class AdnuntiusAdsUnit { - - @JsonProperty("auId") - String auId; - - @JsonProperty("targetId") - String targetId; - - String html; - - @JsonProperty("responseId") - String responseId; - - List ads; - - List deals; -} 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/response/AdnuntiusResponse.java b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java index 6ce62d9afdb..ca4a6d5fe5f 100644 --- a/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java +++ b/src/main/java/org/prebid/server/bidder/adnuntius/model/response/AdnuntiusResponse.java @@ -9,5 +9,5 @@ public class AdnuntiusResponse { @JsonProperty("adUnits") - List adsUnits; + List adUnits; } 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/adocean/AdoceanBidder.java b/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java index 29c0a385d69..63ad320ca4e 100644 --- a/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java +++ b/src/main/java/org/prebid/server/bidder/adocean/AdoceanBidder.java @@ -123,14 +123,14 @@ private boolean addRequestAndCheckIfDuplicates(List> httpReque final List queryParams = uriBuilder.getQueryParams(); final String masterId = queryParams.stream() - .filter(param -> param.getName().equals("id")) + .filter(param -> "id".equals(param.getName())) .findFirst() .map(NameValuePair::getValue) .orElse(null); if (masterId != null && masterId.equals(extImpAdocean.getMasterId())) { final boolean isExistingSlaveId = queryParams.stream() - .filter(param -> param.getName().equals("aid")) + .filter(param -> "aid".equals(param.getName())) .map(param -> param.getValue().split(":")[0]) .anyMatch(slaveId -> slaveId.equals(extImpAdocean.getSlaveId())); if (isExistingSlaveId) { @@ -306,7 +306,7 @@ public Result> makeBids(BidderCall httpCall, BidRequest bi } final Map auctionIds = params != null ? params.stream() - .filter(param -> param.getName().equals("aid")) + .filter(param -> "aid".equals(param.getName())) .map(param -> param.getValue().split(":")) .collect(Collectors.toMap(name -> name[0], value -> value[1])) : null; @@ -319,7 +319,7 @@ public Result> makeBids(BidderCall httpCall, BidRequest bi } final List bidderBids = adoceanResponses.stream() - .filter(adoceanResponse -> !adoceanResponse.getError().equals("true")) + .filter(adoceanResponse -> !"true".equals(adoceanResponse.getError())) .filter(adoceanResponse -> StringUtils.isNotBlank(MapUtils.getString(auctionIds, adoceanResponse.getId()))) .map(adoceanResponse -> BidderBid.of(createBid(auctionIds, adoceanResponse), BidType.banner, diff --git a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java b/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java deleted file mode 100644 index 761a2e21bc4..00000000000 --- a/src/main/java/org/prebid/server/bidder/adoppler/AdopplerBidder.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.prebid.server.bidder.adoppler; - -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.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.lang3.StringUtils; -import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.adoppler.model.AdopplerResponseAdsExt; -import org.prebid.server.bidder.adoppler.model.AdopplerResponseExt; -import org.prebid.server.bidder.adoppler.model.AdopplerResponseVideoAdsExt; -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.adoppler.ExtImpAdoppler; -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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class AdopplerBidder implements Bidder { - - private static final TypeReference> ADOPPLER_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; - private static final String DEFAULT_CLIENT = "app"; - - private final String endpointTemplate; - private final JacksonMapper mapper; - - public AdopplerBidder(String endpointTemplate, JacksonMapper mapper) { - this.endpointTemplate = HttpUtil.validateUrl(Objects.requireNonNull(endpointTemplate)); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public Result>> makeHttpRequests(BidRequest request) { - final List errors = new ArrayList<>(); - final List> result = new ArrayList<>(); - - for (Imp imp : request.getImp()) { - try { - final ExtImpAdoppler validExtImp = parseAndValidateImpExt(imp); - final String updateRequestId = request.getId() + "-" + validExtImp.getAdunit(); - final BidRequest updateRequest = request.toBuilder().id(updateRequestId).build(); - final String url = resolveUrl(validExtImp); - - result.add(createSingleRequest(imp, updateRequest, url)); - } catch (PreBidException e) { - errors.add(BidderError.badInput(e.getMessage())); - } - } - return Result.of(result, errors); - } - - private ExtImpAdoppler parseAndValidateImpExt(Imp imp) { - final ExtImpAdoppler extImpAdoppler; - try { - extImpAdoppler = mapper.mapper().convertValue(imp.getExt(), ADOPPLER_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - if (StringUtils.isBlank(extImpAdoppler.getAdunit())) { - throw new PreBidException("adunit parameter is required for adoppler bidder"); - } - return extImpAdoppler; - } - - private String resolveUrl(ExtImpAdoppler extImp) { - final String client = extImp.getClient(); - - try { - final String accountIdMacro = StringUtils.isBlank(client) - ? DEFAULT_CLIENT - : HttpUtil.encodeUrl(client); - - return endpointTemplate - .replace("{{AccountID}}", accountIdMacro) - .replace("{{AdUnit}}", HttpUtil.encodeUrl(extImp.getAdunit())); - } catch (Exception e) { - throw new PreBidException(e.getMessage()); - } - } - - private HttpRequest createSingleRequest(Imp imp, BidRequest request, String url) { - final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); - final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); - return HttpRequest.builder() - .method(HttpMethod.POST) - .uri(url) - .headers(headers) - .body(mapper.encodeToBytes(outgoingRequest)) - .payload(outgoingRequest) - .build(); - } - - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - try { - final BidResponse bidResponse = decodeBodyToBidResponse(httpCall); - final Map impTypes = getImpTypes(bidRequest); - final List bidderBids = bidResponse.getSeatbid().stream() - .filter(Objects::nonNull) - .map(SeatBid::getBid) - .filter(Objects::nonNull) - .flatMap(Collection::stream) - .map(bid -> createBid(bid, impTypes, bidResponse.getCur())) - .toList(); - return Result.withValues(bidderBids); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - } - - private BidResponse decodeBodyToBidResponse(BidderCall httpCall) { - try { - return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - } catch (DecodeException e) { - throw new PreBidException("invalid body: " + e.getMessage()); - } - } - - private Map getImpTypes(BidRequest bidRequest) { - final Map impTypes = new HashMap<>(); - for (Imp imp : bidRequest.getImp()) { - final String impId = imp.getId(); - - if (imp.getBanner() != null) { - impTypes.put(impId, BidType.banner); - } else if (imp.getVideo() != null) { - impTypes.put(impId, BidType.video); - } else if (imp.getAudio() != null) { - impTypes.put(impId, BidType.audio); - } else if (imp.getXNative() != null) { - impTypes.put(impId, BidType.xNative); - } - } - return impTypes; - } - - private BidderBid createBid(Bid bid, Map impTypes, String currency) { - final String bidImpId = bid.getImpid(); - - if (impTypes.get(bidImpId) == null) { - throw new PreBidException("unknown impId: " + bidImpId); - } - if (impTypes.get(bidImpId) == BidType.video) { - validateVideoBidExt(bid); - } - - return BidderBid.of(bid, impTypes.get(bidImpId), currency); - } - - private void validateVideoBidExt(Bid bid) { - final ObjectNode extNode = bid.getExt(); - final AdopplerResponseExt ext = extNode != null ? parseResponseExt(extNode) : null; - final AdopplerResponseAdsExt adsExt = ext != null ? ext.getAds() : null; - final AdopplerResponseVideoAdsExt videoAdsExt = adsExt != null ? adsExt.getVideo() : null; - if (videoAdsExt == null) { - throw new PreBidException("$.seatbid.bid.ext.ads.video required"); - } - } - - private AdopplerResponseExt parseResponseExt(ObjectNode ext) { - try { - return mapper.mapper().treeToValue(ext, AdopplerResponseExt.class); - } catch (JsonProcessingException e) { - throw new PreBidException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseAdsExt.java b/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseAdsExt.java deleted file mode 100644 index ba9df1a3034..00000000000 --- a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseAdsExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.adoppler.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdopplerResponseAdsExt { - - AdopplerResponseVideoAdsExt video; -} diff --git a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseExt.java b/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseExt.java deleted file mode 100644 index 7165870d695..00000000000 --- a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.adoppler.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdopplerResponseExt { - - AdopplerResponseAdsExt ads; -} diff --git a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseVideoAdsExt.java b/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseVideoAdsExt.java deleted file mode 100644 index 8ab0c5bee59..00000000000 --- a/src/main/java/org/prebid/server/bidder/adoppler/model/AdopplerResponseVideoAdsExt.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.bidder.adoppler.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class AdopplerResponseVideoAdsExt { - - Integer duration; -} 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/adquery/model/response/AdQueryDataResponse.java b/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryDataResponse.java index a7bb6b85609..0880a2d667c 100644 --- a/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryDataResponse.java +++ b/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryDataResponse.java @@ -8,7 +8,7 @@ import java.util.List; @Builder(toBuilder = true) -@Value(staticConstructor = "of") +@Value public class AdQueryDataResponse { @JsonProperty("requestId") diff --git a/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryResponse.java b/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryResponse.java index 0f97f113d7d..ca8fc5dbb81 100644 --- a/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryResponse.java +++ b/src/main/java/org/prebid/server/bidder/adquery/model/response/AdQueryResponse.java @@ -7,4 +7,3 @@ public class AdQueryResponse { AdQueryDataResponse data; } - 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/aduptech/AduptechBidder.java b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java new file mode 100644 index 00000000000..d6a826e9977 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/aduptech/AduptechBidder.java @@ -0,0 +1,172 @@ +package org.prebid.server.bidder.aduptech; + +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.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.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.Currency; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class AduptechBidder implements Bidder { + + private static final String COMPONENT_ID_HEADER = "Componentid"; + private static final String COMPONENT_ID_HEADER_VALUE = "prebid-java"; + private static final String DEFAULT_BID_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + private final String targetCurrency; + + public AduptechBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService, + String targetCurrency) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.targetCurrency = validateCurrency(targetCurrency); + } + + private static String validateCurrency(String code) { + try { + Currency.getInstance(code); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid extra info: invalid TargetCurrency %s".formatted(code)); + } + return code.toUpperCase(); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(request.getImp().size()); + for (Imp imp : request.getImp()) { + try { + modifiedImps.add(modifyImp(imp, request)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = request.toBuilder().imp(modifiedImps).build(); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + makeHeaders(), + endpointUrl, + mapper); + + return Result.withValue(httpRequest); + } + + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + Price impFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + impFloorPrice = BidderUtil.isValidPrice(impFloorPrice) + && !targetCurrency.equalsIgnoreCase(impFloorPrice.getCurrency()) + ? convertBidFloor(impFloorPrice, bidRequest) + : impFloorPrice; + + return imp.toBuilder() + .bidfloor(impFloorPrice.getValue()) + .bidfloorcur(impFloorPrice.getCurrency()) + .build(); + } + + private Price convertBidFloor(Price impFloorPrice, BidRequest bidRequest) { + try { + return convertToTargetCurrency(impFloorPrice.getValue(), bidRequest, impFloorPrice.getCurrency()); + } catch (PreBidException e) { + final BigDecimal defaultCurrencyBidFloor = currencyConversionService.convertCurrency( + impFloorPrice.getValue(), + bidRequest, + impFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + return convertToTargetCurrency(defaultCurrencyBidFloor, bidRequest, DEFAULT_BID_CURRENCY); + } + } + + private Price convertToTargetCurrency(BigDecimal impFloorPrice, BidRequest bidRequest, String fromCurrency) { + final BigDecimal convertedFloor = currencyConversionService.convertCurrency( + impFloorPrice, + bidRequest, + fromCurrency, + targetCurrency); + + return Price.of(targetCurrency, convertedFloor); + } + + private static MultiMap makeHeaders() { + return HttpUtil.headers().add(COMPONENT_ID_HEADER, COMPONENT_ID_HEADER_VALUE); + } + + @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 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) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(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 markupType) { + return switch (markupType) { + case 1 -> BidType.banner; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unknown markup type: " + markupType); + }; + } +} 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..5b9d825be63 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)) { @@ -233,4 +233,3 @@ private static BidType getBidType(String impId, List imps) { return BidType.banner; } } - diff --git a/src/main/java/org/prebid/server/bidder/adverxo/AdverxoBidder.java b/src/main/java/org/prebid/server/bidder/adverxo/AdverxoBidder.java new file mode 100644 index 00000000000..1880a2e9ad4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/adverxo/AdverxoBidder.java @@ -0,0 +1,183 @@ +package org.prebid.server.bidder.adverxo; + +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; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.adverxo.ExtImpAdverxo; +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.stream.Collectors; + +public class AdverxoBidder implements Bidder { + + private static final TypeReference> ADVERXO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String ADUNIT_MACROS_ENDPOINT = "{{adUnitId}}"; + private static final String AUTH_MACROS_ENDPOINT = "{{auth}}"; + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + + public AdverxoBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = mapper; + this.currencyConversionService = currencyConversionService; + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> requests = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpAdverxo extImp = parseImpExt(imp); + final String endpoint = resolveEndpoint(extImp); + final Imp modifiedImp = modifyImp(request, imp); + final BidRequest outgoingRequest = createRequest(request, modifiedImp); + + requests.add(BidderUtil.defaultRequest(outgoingRequest, endpoint, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(requests, errors); + } + + private ExtImpAdverxo parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADVERXO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing ext.imp.bidder: " + e.getMessage()); + } + } + + private String resolveEndpoint(ExtImpAdverxo extImp) { + return endpointUrl + .replace(ADUNIT_MACROS_ENDPOINT, Objects.toString(extImp.getAdUnitId(), "0")) + .replace(AUTH_MACROS_ENDPOINT, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getAuth()))); + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest); + + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + + private static BidRequest createRequest(BidRequest originalRequest, Imp modifiedImp) { + return originalRequest.toBuilder() + .imp(Collections.singletonList(modifiedImp)) + .build(); + } + + @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 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 -> makeBid(bid, bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private BidderBid makeBid(Bid bid, String currency) { + final BidType bidType = getBidType(bid.getMtype()); + final String resolvedAdm = bidType == BidType.xNative ? resolveAdm(bid.getAdm(), bid.getPrice()) : bid.getAdm(); + final Bid processedBid = processBidMacros(bid, resolvedAdm); + + return BidderBid.of(processedBid, bidType, currency); + } + + private static BidType getBidType(Integer mType) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException("Unsupported mType " + mType); + }; + } + + private static Bid processBidMacros(Bid bid, String adm) { + final String price = bid.getPrice() != null ? bid.getPrice().toPlainString() : "0"; + + return bid.toBuilder() + .adm(replaceMacro(adm, price)) + .build(); + } + + private static String replaceMacro(String input, String value) { + return input != null ? input.replace(PRICE_MACRO, value) : null; + } + + private static String resolveAdm(String bidAdm, BigDecimal price) { + return StringUtils.isNotBlank(bidAdm) ? bidAdm.replace("${AUCTION_PRICE}", String.valueOf(price)) : bidAdm; + } +} 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/afront/AfrontBidder.java b/src/main/java/org/prebid/server/bidder/afront/AfrontBidder.java new file mode 100644 index 00000000000..3b71087d380 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/afront/AfrontBidder.java @@ -0,0 +1,146 @@ +package org.prebid.server.bidder.afront; + +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 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.afront.ExtImpAfront; +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 AfrontBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String SOURCE_ID_MACRO = "{{SourceId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AfrontBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final ExtImpAfront extImp; + try { + extImp = parseImpExt(request.getImp().getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final String resolvedEndpoint = resolveEndpoint(extImp); + final BidRequest outgoingRequest = modifyRequest(request); + final HttpRequest httpRequest = + BidderUtil.defaultRequest(outgoingRequest, makeHeaders(request.getDevice()), resolvedEndpoint, mapper); + + return Result.withValue(httpRequest); + } + + private ExtImpAfront parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("ext.bidder not provided"); + } + } + + private String resolveEndpoint(ExtImpAfront extImp) { + return endpointUrl + .replace(ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(extImp.getAccountId())) + .replace(SOURCE_ID_MACRO, HttpUtil.encodeUrl(extImp.getSourceId())); + } + + private static BidRequest modifyRequest(BidRequest request) { + final List modifiedImps = request.getImp().stream() + .map(imp -> imp.toBuilder().ext(null).build()) + .toList(); + + return request.toBuilder() + .imp(modifiedImps) + .build(); + } + + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers() + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + } + + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + return Result.withValues(extractBids(bidRequest, 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) { + final Map imps = 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 -> BidderBid.of(bid, getBidType(bid.getImpid(), imps), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(String impId, Map imps) { + final Imp imp = imps.get(impId); + if (imp != null) { + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + return BidType.banner; + } +} 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..05d89ae67e5 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) { @@ -116,8 +115,6 @@ private static BidType resolveBidType(Bid bid) throws PreBidException { 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 in multi-format: %s" .formatted(bid.getImpid())); }; diff --git a/src/main/java/org/prebid/server/bidder/aja/proto/ExtImpAja.java b/src/main/java/org/prebid/server/bidder/aja/proto/ExtImpAja.java index 1cd75e187bd..33f9025d415 100644 --- a/src/main/java/org/prebid/server/bidder/aja/proto/ExtImpAja.java +++ b/src/main/java/org/prebid/server/bidder/aja/proto/ExtImpAja.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.aja.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAja { @JsonProperty("asi") diff --git a/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java b/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java new file mode 100644 index 00000000000..c214112735e --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/akcelo/AkceloBidder.java @@ -0,0 +1,180 @@ +package org.prebid.server.bidder.akcelo; + +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.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.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.ExtPublisher; +import org.prebid.server.proto.openrtb.ext.request.ExtPublisherPrebid; +import org.prebid.server.proto.openrtb.ext.request.akcelo.ExtImpAkcelo; +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 AkceloBidder implements Bidder { + + private static final TypeReference> AKCELO_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BIDDER_NAME = "akcelo"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public AkceloBidder(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 List modifiedImps = new ArrayList<>(); + + final ExtImpAkcelo firstExtImp; + try { + firstExtImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + for (final Imp imp : imps) { + modifiedImps.add(modifyImp(imp)); + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, firstExtImp.getSiteId()); + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } + + private ExtImpAkcelo parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), AKCELO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp) { + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder"))) + .build(); + } + + private BidRequest modifyRequest(BidRequest request, List imps, String siteId) { + return request.toBuilder() + .imp(imps) + .site(modifySite(request.getSite(), siteId)) + .build(); + } + + private Site modifySite(Site site, String siteId) { + final Publisher publisher = Optional.ofNullable(site) + .map(Site::getPublisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .ext(ExtPublisher.of(ExtPublisherPrebid.of(siteId))) + .build(); + + return Optional.ofNullable(site) + .map(Site::toBuilder) + .orElseGet(Site::builder) + .publisher(publisher) + .build(); + } + + @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 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 -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType == null ? null : BidderBid.of(bid, bidType, currency); + } + + private BidType getBidType(Bid bid, List errors) { + final Integer mType = bid.getMtype(); + if (mType != null) { + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + default -> { + errors.add(BidderError.badServerResponse("unable to get media type " + mType)); + yield null; + } + }; + } + + return getExtBidPrebidType(bid, errors); + } + + private BidType getExtBidPrebidType(Bid bid, List errors) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get("prebid")) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseGet(() -> { + errors.add(BidderError.badServerResponse("missing media type for bid " + bid.getId())); + return null; + }); + } + + 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/algorix/AlgorixBidder.java b/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java index 43549c101e8..301f1fec17d 100644 --- a/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java +++ b/src/main/java/org/prebid/server/bidder/algorix/AlgorixBidder.java @@ -46,11 +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 int FIRST_INDEX = 0; + 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 final String endpointUrl; private final JacksonMapper mapper; @@ -115,7 +113,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..f1a48242f63 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<>() { @@ -69,13 +67,16 @@ private ExtImpAlkimi parseImpExt(Imp imp) { private Imp updateImp(Imp imp, ExtImpAlkimi extImpAlkimi) { final Price bidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + final ObjectNode newExt = imp.getExt().deepCopy(); + newExt.replace("bidder", makeImpExt(imp, extImpAlkimi)); + return imp.toBuilder() .bidfloor(BidderUtil.isValidPrice(bidFloorPrice) ? bidFloorPrice.getValue() : extImpAlkimi.getBidFloor()) .instl(extImpAlkimi.getInstl()) .exp(extImpAlkimi.getExp()) - .ext(makeImpExt(imp, extImpAlkimi)) + .ext(newExt) .build(); } @@ -84,7 +85,7 @@ private ObjectNode makeImpExt(Imp imp, ExtImpAlkimi extImpAlkimi) { extBuilder.adUnitCode(imp.getId()); - return mapper.mapper().valueToTree(ExtPrebid.of(null, extBuilder.build())); + return mapper.mapper().valueToTree(extBuilder.build()); } @Override 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..f9d309a3495 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,16 @@ private BidderBid createBidderBid(Bid bid, String cur, List errors) errors.add(BidderError.badInput(e.getMessage())); return null; } - - return BidderBid.of(bid, getBidType(amxBidExt), cur); + return BidderBid.of( + resolveBid(bid, amxBidExt.getDemandSource()), + getBidType(amxBidExt), + amxBidExt.getBidderCode(), + 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 +201,35 @@ 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..06a37460655 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java @@ -26,6 +26,7 @@ import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtAppnexus; import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtCreative; import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtVideo; +import org.prebid.server.bidder.appnexus.proto.AppnexusExtImp; import org.prebid.server.bidder.appnexus.proto.AppnexusImpExt; import org.prebid.server.bidder.appnexus.proto.AppnexusImpExtAppnexus; import org.prebid.server.bidder.appnexus.proto.AppnexusKeyVal; @@ -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/AppnexusBidExtCreative.java b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtCreative.java index d1d515c506e..971850daeda 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtCreative.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtCreative.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.appnexus.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AppnexusBidExtCreative { AppnexusBidExtVideo video; diff --git a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtVideo.java b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtVideo.java index b27703a2d84..bbfb38e5883 100644 --- a/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtVideo.java +++ b/src/main/java/org/prebid/server/bidder/appnexus/proto/AppnexusBidExtVideo.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.appnexus.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AppnexusBidExtVideo { Integer duration; 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/audiencenetwork/proto/AudienceNetworkAdMarkup.java b/src/main/java/org/prebid/server/bidder/audiencenetwork/proto/AudienceNetworkAdMarkup.java index 7440b4cd7d1..8b7d2634291 100644 --- a/src/main/java/org/prebid/server/bidder/audiencenetwork/proto/AudienceNetworkAdMarkup.java +++ b/src/main/java/org/prebid/server/bidder/audiencenetwork/proto/AudienceNetworkAdMarkup.java @@ -1,9 +1,7 @@ package org.prebid.server.bidder.audiencenetwork.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor @Value public class AudienceNetworkAdMarkup { diff --git a/src/main/java/org/prebid/server/bidder/avocet/model/AvocetBidExtension.java b/src/main/java/org/prebid/server/bidder/avocet/model/AvocetBidExtension.java index 3c740531147..32484c75e90 100644 --- a/src/main/java/org/prebid/server/bidder/avocet/model/AvocetBidExtension.java +++ b/src/main/java/org/prebid/server/bidder/avocet/model/AvocetBidExtension.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.avocet.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AvocetBidExtension { Integer duration; diff --git a/src/main/java/org/prebid/server/bidder/avocet/model/AvocetResponseExt.java b/src/main/java/org/prebid/server/bidder/avocet/model/AvocetResponseExt.java index 72a5cc4cf7c..75d5a91835d 100644 --- a/src/main/java/org/prebid/server/bidder/avocet/model/AvocetResponseExt.java +++ b/src/main/java/org/prebid/server/bidder/avocet/model/AvocetResponseExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.avocet.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class AvocetResponseExt { AvocetBidExtension avocet; 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..265ce948214 100644 --- a/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/axis/AxisBidder.java @@ -47,27 +47,26 @@ public Result>> makeHttpRequests(BidRequest request final List> httpRequests = new ArrayList<>(); for (Imp imp : request.getImp()) { - final ExtImpAxis extImpAxis; try { - extImpAxis = parseImpExt(imp); + validateImpExt(imp); } catch (PreBidException e) { continue; } - httpRequests.add(makeRequest(request, imp, extImpAxis)); + httpRequests.add(makeRequest(request, imp)); } return Result.withValues(httpRequests); } - private ExtImpAxis parseImpExt(Imp imp) { + private void validateImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), ADMAN_EXT_TYPE_REFERENCE).getBidder(); + mapper.mapper().convertValue(imp.getExt(), ADMAN_EXT_TYPE_REFERENCE); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } } - 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(); @@ -132,4 +131,3 @@ private BidType getBidType(String impId, List imps) { throw new PreBidException("Failed to find impression \"%s\"".formatted(impId)); } } - 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..e7bbf291e38 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())); } @@ -120,4 +120,3 @@ private static BidType getMediaType(String impId, List imps) { return BidType.banner; } } - 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..79011887eb9 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(); @@ -339,7 +339,7 @@ private List getVideoRequests(BidRequest bidRequest, .appId(appId); final String responseType; - if (videoResponseType != null && videoResponseType.equals(NURL_VIDEO_TYPE)) { + if (NURL_VIDEO_TYPE.equals(videoResponseType)) { requestBuilder.isPrebid(true); responseType = NURL_VIDEO_TYPE; } else { @@ -364,7 +364,7 @@ private List getVideoRequests(BidRequest bidRequest, if (devicetype == null || devicetype == 0) { deviceBuilder.devicetype(bidRequest.getSite() != null ? 2 : 1); } - if (StringUtils.isBlank(device.getIp()) && responseType.equals(ADM_VIDEO_TYPE)) { + if (StringUtils.isBlank(device.getIp()) && ADM_VIDEO_TYPE.equals(responseType)) { deviceBuilder.ip(FAKE_IP); } @@ -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/beachfront/model/BeachfrontSize.java b/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSize.java index c60ae7ce809..004b140448f 100644 --- a/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSize.java +++ b/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSize.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.beachfront.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class BeachfrontSize { Integer w; diff --git a/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSlot.java b/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSlot.java index 899b8315fd3..178542d294f 100644 --- a/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSlot.java +++ b/src/main/java/org/prebid/server/bidder/beachfront/model/BeachfrontSlot.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.beachfront.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class BeachfrontSlot { String slot; 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/bidtheatre/BidTheatreBidder.java b/src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java new file mode 100644 index 00000000000..1e1fc625175 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/bidtheatre/BidTheatreBidder.java @@ -0,0 +1,118 @@ +package org.prebid.server.bidder.bidtheatre; + +import com.fasterxml.jackson.core.type.TypeReference; +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.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.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 BidTheatreBidder implements Bidder { + + 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 BidTheatreBidder(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 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) + .filter(Objects::nonNull) + .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); + if (mediaType == null) { + errors.add(BidderError.badServerResponse("Failed to parse impression \"%s\" mediatype" + .formatted(bid.getImpid()))); + 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) { + return Optional.ofNullable(bid.getExt()) + .map(this::parseBidExt) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getType) + .orElse(null); + } + + private ExtPrebid parseBidExt(ObjectNode ext) { + try { + return mapper.mapper().convertValue(ext, EXT_PREBID_TYPE_REFERENCE); + } catch (IllegalArgumentException e) { + return null; + } + } +} 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/blis/BlisBidder.java b/src/main/java/org/prebid/server/bidder/blis/BlisBidder.java new file mode 100644 index 00000000000..10e9ee547b4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/blis/BlisBidder.java @@ -0,0 +1,138 @@ +package org.prebid.server.bidder.blis; + +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.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.blis.ExtImpBlis; +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.stream.Collectors; + +public class BlisBidder implements Bidder { + + private static final TypeReference> BLIS_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String AUCTION_PRICE_MACRO = "${AUCTION_PRICE}"; + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BlisBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final String supplyId; + try { + supplyId = parseImpExt(request.getImp().getFirst()).getSupplyId(); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(BidderUtil.defaultRequest(request, makeHeaders(supplyId), makeUrl(supplyId), mapper)); + } + + private ExtImpBlis parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), BLIS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext: " + e.getMessage()); + } + } + + private static MultiMap makeHeaders(String supplyId) { + return HttpUtil.headers().add("X-Supply-Partner-Id", supplyId); + } + + private String makeUrl(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); + final List errors = new ArrayList<>(); + 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 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) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(resolveMacros(bid), bidType, currency) + : 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(), AUCTION_PRICE_MACRO, priceAsString)) + .adm(StringUtils.replace(bid.getAdm(), AUCTION_PRICE_MACRO, priceAsString)) + .burl(StringUtils.replace(bid.getBurl(), AUCTION_PRICE_MACRO, priceAsString)) + .build(); + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse( + "Failed to parse media type of impression ID " + bid.getImpid())); + yield null; + } + }; + } +} 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/boldwinrapid/BoldwinRapidBidder.java b/src/main/java/org/prebid/server/bidder/boldwinrapid/BoldwinRapidBidder.java new file mode 100644 index 00000000000..ca6da3a8653 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/boldwinrapid/BoldwinRapidBidder.java @@ -0,0 +1,140 @@ +package org.prebid.server.bidder.boldwinrapid; + +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.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.boldwinrapid.ExtImpBoldwinRapid; +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 BoldwinRapidBidder implements Bidder { + + private static final TypeReference> BOLDWIN_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String PLACEMENT_ID_MACRO = "{{PlacementID}}"; + private static final String HOST_HEADER_VALUE = "rtb.beardfleet.com"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public BoldwinRapidBidder(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<>(); + final MultiMap headers = makeHeaders(request.getDevice()); + + for (Imp imp : request.getImp()) { + try { + final ExtImpBoldwinRapid extImp = parseImpExt(imp); + final String resolvedEndpoint = resolveEndpoint(extImp); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + + httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, headers, resolvedEndpoint, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpBoldwinRapid parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), BOLDWIN_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext: " + e.getMessage()); + } + } + + private String resolveEndpoint(ExtImpBoldwinRapid extImp) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getPid()))) + .replace(PLACEMENT_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getTid()))); + } + + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers() + .set(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5") + .set("Host", HOST_HEADER_VALUE); + + 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()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, "IP", device.getIp()); + } + + return headers; + } + + @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 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, 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( + "Unable to fetch mediaType in multi-format: " + bid.getImpid()); + }; + } +} 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/connatix/ConnatixBidder.java b/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java new file mode 100644 index 00000000000..aef2abcaf61 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java @@ -0,0 +1,270 @@ +package org.prebid.server.bidder.connatix; + +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.Banner; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.User; +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.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +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.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.ExtApp; +import org.prebid.server.proto.openrtb.ext.request.connatix.ExtImpConnatix; +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.net.URISyntaxException; +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 ConnatixBidder implements Bidder { + + private static final TypeReference> CONNATIX_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String BIDDER_CURRENCY = "USD"; + private static final String FORMATTING = "%s-%s"; + private static final String GPID_KEY = "gpid"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + private final CurrencyConversionService currencyConversionService; + + public ConnatixBidder(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 request) { + final Device device = request.getDevice(); + + if (device == null + || (device.getIp() == null && device.getIpv6() == null)) { + return Result.withError(BidderError.badInput("Device IP is required")); + } + + final String optimalEndpointUrl; + try { + optimalEndpointUrl = getOptimalEndpointUrl(request); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + final String displayManagerVer = buildDisplayManagerVersion(request); + final MultiMap headers = resolveHeaders(device); + + final List> httpRequests = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpConnatix extImpConnatix = parseExtImp(imp); + final Imp modifiedImp = modifyImp(imp, extImpConnatix, displayManagerVer, request); + + httpRequests.add(makeHttpRequest(request, modifiedImp, headers, optimalEndpointUrl)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private String getOptimalEndpointUrl(BidRequest request) { + final Optional dataCenterCode = getUserId(request).map(ConnatixBidder::getDataCenterCode); + + if (dataCenterCode.isEmpty()) { + return endpointUrl; + } + + try { + return new URIBuilder(endpointUrl) + .addParameter("dc", dataCenterCode.get()) + .build() + .toString(); + } catch (URISyntaxException e) { + throw new PreBidException(e.getMessage()); + } + } + + private static Optional getUserId(BidRequest request) { + return Optional.ofNullable(request.getUser()) + .map(User::getBuyeruid) + .filter(StringUtils::isNotBlank) + .map(String::trim); + } + + private static String getDataCenterCode(String usedId) { + if (usedId.startsWith("1-")) { + return "us-east-2"; + } else if (usedId.startsWith("2-")) { + return "us-west-2"; + } else if (usedId.startsWith("3-")) { + return "eu-west-1"; + } + + return null; + } + + private static String buildDisplayManagerVersion(BidRequest request) { + return Optional.ofNullable(request.getApp()) + .map(App::getExt) + .map(ExtApp::getPrebid) + .filter(prebid -> ObjectUtils.allNotNull(prebid.getSource(), prebid.getVersion())) + .map(prebid -> FORMATTING.formatted(prebid.getSource(), prebid.getVersion())) + .orElse(StringUtils.EMPTY); + } + + 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.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + return headers; + } + + private ExtImpConnatix parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), CONNATIX_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpConnatix extImpConnatix, String displayManagerVer, BidRequest request) { + final Price bidFloorPrice = resolveBidFloor(imp, request); + + final ObjectNode impExt = imp.getExt() != null + ? imp.getExt().deepCopy() + : mapper.mapper().createObjectNode(); + + impExt.remove("bidder"); + impExt.set("connatix", mapper.mapper().valueToTree(extImpConnatix)); + + return imp.toBuilder() + .ext(impExt) + .banner(modifyImpBanner(imp.getBanner())) + .displaymanagerver(StringUtils.isBlank(imp.getDisplaymanagerver()) + && StringUtils.isNotBlank(displayManagerVer) + ? displayManagerVer + : imp.getDisplaymanagerver()) + .bidfloor(bidFloorPrice.getValue()) + .bidfloorcur(bidFloorPrice.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } + + private Banner modifyImpBanner(Banner banner) { + if (banner == null) { + return null; + } + + if (banner.getW() == null && banner.getH() == null && CollectionUtils.isNotEmpty(banner.getFormat())) { + final Format firstFormat = banner.getFormat().getFirst(); + return banner.toBuilder() + .w(firstFormat.getW()) + .h(firstFormat.getH()) + .build(); + } + return banner; + } + + private HttpRequest makeHttpRequest(BidRequest request, + Imp imp, + MultiMap headers, + String optimalEndpointUrl) { + + final BidRequest outgoingRequest = request.toBuilder().imp(List.of(imp)).build(); + return BidderUtil.defaultRequest(outgoingRequest, headers, optimalEndpointUrl, 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 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), BIDDER_CURRENCY)) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get("connatix")) + .map(cnx -> cnx.get("mediaType")) + .map(JsonNode::asText) + .filter(type -> Objects.equals(type, "video")) + .map(ignored -> BidType.video) + .orElse(BidType.banner); + } +} 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..5b70032a98d 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,220 @@ 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())); - } - - 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()); + if (imps.isEmpty()) { + return Result.withErrors(errors); } - 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; + } - return headers; + private static MultiMap resolveHeaders() { + return HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); } @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final ConsumableBidResponse consumableResponse; + @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 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 errors = new ArrayList<>(); + + return CompositeBidderResponse.builder() + .bids(extractConsumableBids(bidRequest, bidResponse, errors)) + .errors(errors) + .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/contxtful/ContxtfulBidder.java b/src/main/java/org/prebid/server/bidder/contxtful/ContxtfulBidder.java new file mode 100644 index 00000000000..cc05acadcd4 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/ContxtfulBidder.java @@ -0,0 +1,224 @@ +package org.prebid.server.bidder.contxtful; + +import com.fasterxml.jackson.core.type.TypeReference; +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 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.contxtful.request.ContxtfulBidRequest; +import org.prebid.server.bidder.contxtful.request.ContxtfulBidRequestParams; +import org.prebid.server.bidder.contxtful.request.ContxtfulBidderRequest; +import org.prebid.server.bidder.contxtful.request.ContxtfulCompositeRequest; +import org.prebid.server.bidder.contxtful.request.ContxtfulConfig; +import org.prebid.server.bidder.contxtful.request.ContxtfulConfigDetails; +import org.prebid.server.bidder.contxtful.response.ContxtfulBid; +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.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; +import org.prebid.server.proto.openrtb.ext.request.contxtful.ExtImpContxtful; +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.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 ContxtfulBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String BIDDER_NAME = "contxtful"; + private static final String DEFAULT_ADAPTER_VERSION = "v1"; + private static final String DEFAULT_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ContxtfulBidder(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 bidRequests = new ArrayList<>(); + String customerId = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpContxtful extImp = parseImpExt(imp); + if (customerId == null) { + customerId = extImp.getCustomerId(); + } + bidRequests.add(ContxtfulBidRequest.of( + BIDDER_NAME, + ContxtfulBidRequestParams.of(extImp.getPlacementId()), imp.getId())); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (CollectionUtils.isEmpty(bidRequests)) { + return Result.withErrors(errors); + } + + final ContxtfulCompositeRequest outgoingRequest = ContxtfulCompositeRequest.builder() + .ortb2Request(request.toBuilder().user(modifyUser(request.getUser())).build()) + .bidRequests(bidRequests) + .bidderRequest(ContxtfulBidderRequest.of(BIDDER_NAME)) + .config(ContxtfulConfig.of(ContxtfulConfigDetails.of(DEFAULT_ADAPTER_VERSION, customerId))) + .build(); + + final HttpRequest httpRequest = HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(customerId)) + .headers(HttpUtil.headers()) + .body(mapper.encodeToBytes(outgoingRequest)) + .payload(outgoingRequest) + .impIds(BidderUtil.impIds(request)) + .build(); + + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private ExtImpContxtful parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext for impression " + imp.getId()); + } + } + + private static User modifyUser(User user) { + if (user == null) { + return null; + } + + final String buyerUid = user.getBuyeruid(); + if (StringUtils.isNotBlank(buyerUid)) { + return user; + } + + return Optional.ofNullable(user.getExt()) + .map(ExtUser::getPrebid) + .map(ExtUserPrebid::getBuyeruids) + .map(buyerUids -> buyerUids.get(BIDDER_NAME)) + .filter(StringUtils::isNotBlank) + .map(uid -> user.toBuilder().buyeruid(uid).build()) + .orElse(user); + } + + private String makeUrl(String customerId) { + return endpointUrl.replace(ACCOUNT_ID_MACRO, HttpUtil.encodeUrl(customerId)); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + try { + final List responseBids = mapper.decodeValue( + httpCall.getResponse().getBody(), + new TypeReference<>() { + }); + return Result.of(extractBids(bidRequest, responseBids, errors), errors); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private static List extractBids(BidRequest bidRequest, + List responseBids, + List errors) { + + if (CollectionUtils.isEmpty(responseBids)) { + return Collections.emptyList(); + } + return bidsFromResponse(bidRequest, responseBids, errors); + } + + private static List bidsFromResponse(BidRequest bidRequest, + List responseBids, + List errors) { + + final Map impsMap = bidRequest.getImp().stream() + .collect(Collectors.toMap(Imp::getId, Function.identity())); + + return responseBids.stream() + .filter(Objects::nonNull) + .map(responseBid -> makeBidderBid(responseBid, impsMap, errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBidderBid(ContxtfulBid responseBid, + Map impsMap, + List errors) { + + final String impId = responseBid.getRequestId(); + if (responseBid.getCpm() == null || impId == null) { + return null; + } + + if (StringUtils.isBlank(responseBid.getMediaType())) { + errors.add(BidderError.badServerResponse("bid %s has no ad media type".formatted(impId))); + return null; + } + + if (StringUtils.isBlank(responseBid.getAdm())) { + errors.add(BidderError.badServerResponse("bid %s has no ad markup".formatted(impId))); + return null; + } + + final Bid bid = Bid.builder() + .id(BIDDER_NAME + "-" + impId) + .impid(impId) + .price(responseBid.getCpm()) + .adm(responseBid.getAdm()) + .w(responseBid.getWidth()) + .h(responseBid.getHeight()) + .crid(responseBid.getCreativeId()) + .nurl(responseBid.getNurl()) + .burl(responseBid.getBurl()) + .lurl(responseBid.getLurl()) + .ext(responseBid.getExt()) + .build(); + + final String currency = Objects.toString(responseBid.getCurrency(), DEFAULT_CURRENCY); + return BidderBid.of(bid, getBidType(impsMap.get(impId)), currency); + } + + private static BidType getBidType(Imp imp) { + if (imp == null) { + return BidType.banner; + } + + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else { + return BidType.banner; + } + } +} + diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequest.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequest.java new file mode 100644 index 00000000000..82d011c01f8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.contxtful.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ContxtfulBidRequest { + + String bidder; + + ContxtfulBidRequestParams params; + + @JsonProperty("bidId") + String bidId; + +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequestParams.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequestParams.java new file mode 100644 index 00000000000..e4898fa5064 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidRequestParams.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.contxtful.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ContxtfulBidRequestParams { + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidderRequest.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidderRequest.java new file mode 100644 index 00000000000..3e3a8e7e254 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulBidderRequest.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.contxtful.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ContxtfulBidderRequest { + + @JsonProperty("bidderCode") + String bidderCode; +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulCompositeRequest.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulCompositeRequest.java new file mode 100644 index 00000000000..dc34fc90c62 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulCompositeRequest.java @@ -0,0 +1,24 @@ +package org.prebid.server.bidder.contxtful.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.BidRequest; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value(staticConstructor = "of") +public class ContxtfulCompositeRequest { + + @JsonProperty("ortb2") + BidRequest ortb2Request; + + @JsonProperty("bidRequests") + List bidRequests; + + @JsonProperty("bidderRequest") + ContxtfulBidderRequest bidderRequest; + + ContxtfulConfig config; +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfig.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfig.java new file mode 100644 index 00000000000..05e61089eea --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfig.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.contxtful.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ContxtfulConfig { + + @JsonProperty("contxtful") + ContxtfulConfigDetails details; +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfigDetails.java b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfigDetails.java new file mode 100644 index 00000000000..0e144646431 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/request/ContxtfulConfigDetails.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.contxtful.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ContxtfulConfigDetails { + + String version; + + String customer; +} diff --git a/src/main/java/org/prebid/server/bidder/contxtful/response/ContxtfulBid.java b/src/main/java/org/prebid/server/bidder/contxtful/response/ContxtfulBid.java new file mode 100644 index 00000000000..d7eafc62b5a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/contxtful/response/ContxtfulBid.java @@ -0,0 +1,56 @@ +package org.prebid.server.bidder.contxtful.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Builder +@Value(staticConstructor = "of") +public class ContxtfulBid { + + @JsonProperty("requestId") + String requestId; + + BigDecimal cpm; + + String currency; + + Integer width; + + Integer height; + + @JsonProperty("creativeId") + String creativeId; + + String adm; + + Integer ttl; + + @JsonProperty("netRevenue") + Boolean netRevenue; + + @JsonProperty("mediaType") + String mediaType; + + @JsonProperty("bidderCode") + String bidderCode; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("traceId") + String traceId; + + BigDecimal random; + + String nurl; + + String burl; + + String lurl; + + ObjectNode ext; +} 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/cpmstar/CpmStarBidder.java b/src/main/java/org/prebid/server/bidder/cpmstar/CpmStarBidder.java index b3dc01cee97..a5b5687c1b4 100644 --- a/src/main/java/org/prebid/server/bidder/cpmstar/CpmStarBidder.java +++ b/src/main/java/org/prebid/server/bidder/cpmstar/CpmStarBidder.java @@ -1,6 +1,8 @@ package org.prebid.server.bidder.cpmstar; 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; @@ -79,7 +81,17 @@ private Imp createImp(ExtImpCpmStar extImpCpmStar, Imp imp) { if (extImpCpmStar == null) { throw new PreBidException("imp id=%s: bidder.ext is null".formatted(imp.getId())); } - return imp.toBuilder().ext(mapper.mapper().valueToTree(extImpCpmStar)).build(); + + final ObjectNode impExt = imp.getExt() != null + ? imp.getExt().deepCopy() + : mapper.mapper().createObjectNode(); + + final JsonNode bidderNode = impExt.remove("bidder"); + if (bidderNode != null && bidderNode.isObject()) { + bidderNode.fields().forEachRemaining(entry -> impExt.set(entry.getKey(), entry.getValue())); + } + + return imp.toBuilder().ext(impExt).build(); } @Override 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..9bf68e89e24 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.ExtIgi; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -44,16 +45,27 @@ 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.builder() + .bids(extractBids(bidResponse)) + .igi(extractIgi(bidResponse)) + .build(); } 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 +106,12 @@ private ObjectNode makeExt(String networkName) { .meta(ExtBidPrebidMeta.builder().networkName(networkName).build()) .build()); } + + private static List extractIgi(CriteoBidResponse bidResponse) { + return Optional.ofNullable(bidResponse) + .map(CriteoBidResponse::getExt) + .map(CriteoExtBidResponse::getIgi) + .filter(CollectionUtils::isNotEmpty) + .orElse(Collections.emptyList()); + } } 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..dda26a73ef7 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/criteo/CriteoExtBidResponse.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.criteo; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; + +import java.util.List; + +@Value(staticConstructor = "of") +public class CriteoExtBidResponse { + + List igi; +} 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/dxkulture/DxKultureBidder.java b/src/main/java/org/prebid/server/bidder/dxkulture/DxKultureBidder.java index 77ead1a4994..ad3c146795d 100644 --- a/src/main/java/org/prebid/server/bidder/dxkulture/DxKultureBidder.java +++ b/src/main/java/org/prebid/server/bidder/dxkulture/DxKultureBidder.java @@ -124,14 +124,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, - List errors) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -142,12 +140,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/elementaltv/ElementalTVBidder.java b/src/main/java/org/prebid/server/bidder/elementaltv/ElementalTVBidder.java new file mode 100644 index 00000000000..17cd2950b7a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/elementaltv/ElementalTVBidder.java @@ -0,0 +1,181 @@ +package org.prebid.server.bidder.elementaltv; + +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.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.lang3.StringUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.elementaltv.model.ElementalTVResponseAdsExt; +import org.prebid.server.bidder.elementaltv.model.ElementalTVResponseExt; +import org.prebid.server.bidder.elementaltv.model.ElementalTVResponseVideoAdsExt; +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.elementaltv.ExtImpElementalTV; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class ElementalTVBidder implements Bidder { + + private static final TypeReference> ELEMENTALTV_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointTemplate; + private final JacksonMapper mapper; + + public ElementalTVBidder(String endpointTemplate, JacksonMapper mapper) { + this.endpointTemplate = HttpUtil.validateUrl(Objects.requireNonNull(endpointTemplate)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + final List> result = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpElementalTV validExtImp = parseAndValidateImpExt(imp); + final String updateRequestId = request.getId() + "-" + validExtImp.getAdunit(); + final BidRequest updateRequest = request.toBuilder().id(updateRequestId).build(); + final String url = resolveUrl(validExtImp); + + result.add(createSingleRequest(imp, updateRequest, url)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return Result.of(result, errors); + } + + private ExtImpElementalTV parseAndValidateImpExt(Imp imp) { + final ExtImpElementalTV extImpElementalTV; + try { + extImpElementalTV = mapper.mapper().convertValue(imp.getExt(), ELEMENTALTV_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + if (StringUtils.isBlank(extImpElementalTV.getAdunit())) { + throw new PreBidException("adunit parameter is required for elementaltv bidder"); + } + return extImpElementalTV; + } + + private String resolveUrl(ExtImpElementalTV extImp) { + try { + return endpointTemplate + .replace("{{AdUnit}}", HttpUtil.encodeUrl(extImp.getAdunit())); + } catch (Exception e) { + throw new PreBidException(e.getMessage()); + } + } + + private HttpRequest createSingleRequest(Imp imp, BidRequest request, String url) { + final BidRequest outgoingRequest = request.toBuilder().imp(Collections.singletonList(imp)).build(); + final MultiMap headers = HttpUtil.headers().add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(url) + .headers(headers) + .body(mapper.encodeToBytes(outgoingRequest)) + .payload(outgoingRequest) + .build(); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final BidResponse bidResponse = decodeBodyToBidResponse(httpCall); + final Map impTypes = getImpTypes(bidRequest); + final List bidderBids = bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(bid -> createBid(bid, impTypes, bidResponse.getCur())) + .toList(); + return Result.withValues(bidderBids); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private BidResponse decodeBodyToBidResponse(BidderCall httpCall) { + try { + return mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + throw new PreBidException("invalid body: " + e.getMessage()); + } + } + + private Map getImpTypes(BidRequest bidRequest) { + final Map impTypes = new HashMap<>(); + for (Imp imp : bidRequest.getImp()) { + final String impId = imp.getId(); + + if (imp.getBanner() != null) { + impTypes.put(impId, BidType.banner); + } else if (imp.getVideo() != null) { + impTypes.put(impId, BidType.video); + } else if (imp.getAudio() != null) { + impTypes.put(impId, BidType.audio); + } else if (imp.getXNative() != null) { + impTypes.put(impId, BidType.xNative); + } + } + return impTypes; + } + + private BidderBid createBid(Bid bid, Map impTypes, String currency) { + final String bidImpId = bid.getImpid(); + + if (impTypes.get(bidImpId) == null) { + throw new PreBidException("unknown impId: " + bidImpId); + } + if (impTypes.get(bidImpId) == BidType.video) { + validateVideoBidExt(bid); + } + + return BidderBid.of(bid, impTypes.get(bidImpId), currency); + } + + private void validateVideoBidExt(Bid bid) { + final ObjectNode extNode = bid.getExt(); + final ElementalTVResponseExt ext = extNode != null ? parseResponseExt(extNode) : null; + final ElementalTVResponseAdsExt adsExt = ext != null ? ext.getAds() : null; + final ElementalTVResponseVideoAdsExt videoAdsExt = adsExt != null ? adsExt.getVideo() : null; + if (videoAdsExt == null) { + throw new PreBidException("$.seatbid.bid.ext.ads.video required"); + } + } + + private ElementalTVResponseExt parseResponseExt(ObjectNode ext) { + try { + return mapper.mapper().treeToValue(ext, ElementalTVResponseExt.class); + } catch (JsonProcessingException e) { + throw new PreBidException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseAdsExt.java b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseAdsExt.java new file mode 100644 index 00000000000..1a23b70eaa9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseAdsExt.java @@ -0,0 +1,9 @@ +package org.prebid.server.bidder.elementaltv.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ElementalTVResponseAdsExt { + + ElementalTVResponseVideoAdsExt video; +} diff --git a/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseExt.java b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseExt.java new file mode 100644 index 00000000000..935d32e01b6 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseExt.java @@ -0,0 +1,9 @@ +package org.prebid.server.bidder.elementaltv.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ElementalTVResponseExt { + + ElementalTVResponseAdsExt ads; +} diff --git a/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseVideoAdsExt.java b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseVideoAdsExt.java new file mode 100644 index 00000000000..76e3457b453 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/elementaltv/model/ElementalTVResponseVideoAdsExt.java @@ -0,0 +1,9 @@ +package org.prebid.server.bidder.elementaltv.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ElementalTVResponseVideoAdsExt { + + Integer duration; +} 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/eplanning/EplanningBidder.java b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java index aab1f5b7e86..99beb0538bf 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/EplanningBidder.java @@ -8,11 +8,15 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; 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.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; @@ -29,6 +33,7 @@ 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.ExtSource; import org.prebid.server.proto.openrtb.ext.request.eplanning.ExtImpEplanning; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.HttpUtil; @@ -45,6 +50,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.Supplier; import java.util.stream.Collectors; @@ -269,6 +275,12 @@ private String resolveRequestUri(BidRequest request, List requestsString uriBuilder.addParameter("app", REQUEST_TARGET_INVENTORY); } + String schain = getSchainParameter(request.getSource()); + if (schain != null) { + schain = schain.replace(" ", "%20"); + uriBuilder.addParameter("sch", schain); + } + return uriBuilder.toString(); } @@ -280,6 +292,53 @@ private static URL parseUrl(String url) { } } + private String getSchainParameter(Source source) { + return Optional.ofNullable(source) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .orElse(null); + } + + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes) || nodes.size() > 2) { + return null; + } + + final StringBuilder schainBuilder = new StringBuilder(); + + schainBuilder.append(schain.getVer()); + schainBuilder.append(","); + schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); + for (SupplyChainNode node : schain.getNodes()) { + schainBuilder.append("!"); + schainBuilder.append(StringUtils.defaultString(node.getAsi())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getSid())); + schainBuilder.append(","); + + schainBuilder.append(node.getHp() != null ? node.getHp() : StringUtils.EMPTY); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getRid())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getName())); + schainBuilder.append(","); + + schainBuilder.append(StringUtils.defaultString(node.getDomain())); + schainBuilder.append(","); + + schainBuilder.append(node.getExt() == null + ? StringUtils.EMPTY + : mapper.encodeToString(node.getExt())); + } + + return schainBuilder.toString(); + } + /** * Converts response to {@link List} of {@link BidderBid}s with {@link List} of errors. * Handles cases when response status is different to OK 200. @@ -337,6 +396,7 @@ private static BidderBid mapToBidderBid(HbResponseSpace hbResponseSpace, HbRespo .price(new BigDecimal(hbResponseAd.getPrice())) .adm(hbResponseAd.getAdM()) .crid(hbResponseAd.getCrId()) + .adomain(Collections.singletonList(hbResponseAd.getAdom())) .w(hbResponseAd.getWidth()) .h(hbResponseAd.getHeight()) .build(), diff --git a/src/main/java/org/prebid/server/bidder/eplanning/model/CleanStepName.java b/src/main/java/org/prebid/server/bidder/eplanning/model/CleanStepName.java index 110fb78e302..988727a5198 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/model/CleanStepName.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/model/CleanStepName.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.eplanning.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CleanStepName { String expression; diff --git a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponse.java b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponse.java index 569606a43c2..d4d2c52de43 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponse.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponse.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.eplanning.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HbResponse { @JsonProperty("sp") diff --git a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseAd.java b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseAd.java index 2d5cee971b0..d951070a537 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseAd.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseAd.java @@ -23,6 +23,8 @@ public class HbResponseAd { @JsonProperty("crid") String crId; + String adom; + @JsonProperty("w") Integer width; diff --git a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseSpace.java b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseSpace.java index d40f4fe0565..06e42089cd8 100644 --- a/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseSpace.java +++ b/src/main/java/org/prebid/server/bidder/eplanning/model/HbResponseSpace.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.eplanning.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HbResponseSpace { @JsonProperty("k") 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..4be70d935fc 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,34 @@ 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(); - 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 +143,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 +239,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)) { @@ -239,7 +261,15 @@ private Bid updateBidWithId(Bid bid) { private static BidType getType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { - return imp.getVideo() != null ? BidType.video : BidType.banner; + if (imp.getAudio() != null) { + return BidType.audio; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } else if (imp.getVideo() != null) { + return BidType.video; + } else { + return BidType.banner; + } } } return BidType.banner; 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/exco/ExcoBidder.java b/src/main/java/org/prebid/server/bidder/exco/ExcoBidder.java new file mode 100644 index 00000000000..41e067def03 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/exco/ExcoBidder.java @@ -0,0 +1,153 @@ +package org.prebid.server.bidder.exco; + +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.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.exco.ExtImpExco; +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; +import java.util.stream.Collectors; + +public class ExcoBidder implements Bidder { + + private static final TypeReference> EXCO_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ExcoBidder(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<>(); + + String publisherId = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpExco extImp = parseImpExt(imp); + modifiedImps.add(imp.toBuilder().tagid(extImp.getTagId()).build()); + publisherId = extImp.getPublisherId(); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } + + private ExtImpExco parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXCO_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid imp.ext for impression %s. Error Information: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private BidRequest modifyRequest(BidRequest request, List imps, String publisherId) { + final Site site = request.getSite(); + final App app = request.getApp(); + + return request.toBuilder() + .imp(imps) + .site(site != null ? modifySite(site, publisherId) : null) + .app(app != null ? modifyApp(app, publisherId) : null) + .build(); + } + + private static Site modifySite(Site site, String publisherId) { + return site.toBuilder().publisher(modifyPublisher(site.getPublisher(), publisherId)).build(); + } + + private static App modifyApp(App app, String publisherId) { + return app.toBuilder().publisher(modifyPublisher(app.getPublisher(), publisherId)).build(); + } + + private static Publisher modifyPublisher(Publisher publisher, String publisherId) { + return Optional.ofNullable(publisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .id(publisherId) + .build(); + } + + @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 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) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(bid, bidType, currency) + : null; + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> { + errors.add(BidderError.badServerResponse( + "unrecognized bid_ad_type in response from exco: " + bid.getMtype())); + yield null; + } + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/feedad/FeedAdBidder.java b/src/main/java/org/prebid/server/bidder/feedad/FeedAdBidder.java new file mode 100644 index 00000000000..5279e2f70b3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/feedad/FeedAdBidder.java @@ -0,0 +1,83 @@ +package org.prebid.server.bidder.feedad; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +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.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class FeedAdBidder implements Bidder { + + private static final String OPENRTB_VERSION = "2.5"; + private static final String X_FA_PBS_ADAPTER_VERSION_HEADER = "X-FA-PBS-Adapter-Version"; + private static final String FEED_AD_ADAPTER_VERSION = "1.0.0"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public FeedAdBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(endpointUrl); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final MultiMap headers = resolveHeaders(bidRequest.getDevice()); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, headers, endpointUrl, mapper)); + } + + private MultiMap resolveHeaders(Device device) { + final MultiMap headers = HttpUtil.headers() + .add(X_FA_PBS_ADAPTER_VERSION_HEADER, FEED_AD_ADAPTER_VERSION) + .add(HttpUtil.X_OPENRTB_VERSION_HEADER, OPENRTB_VERSION); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } + return headers; + } + + @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(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/flatads/FlatadsBidder.java b/src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java new file mode 100644 index 00000000000..11af869d1da --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/flatads/FlatadsBidder.java @@ -0,0 +1,164 @@ +package org.prebid.server.bidder.flatads; + +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.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.flatads.ExtImpFlatads; +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.function.Function; +import java.util.stream.Collectors; + +public class FlatadsBidder implements Bidder { + + private static final TypeReference> FLATADS_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + private static final String TOKEN_ID_MACRO = "{{TokenID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public FlatadsBidder(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()) { + try { + final ExtImpFlatads extImpFlatads = parseImpExt(imp); + final String resolvedEndpoint = resolveEndpoint(extImpFlatads); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + httpRequests.add(makeHttpRequest(outgoingRequest, resolvedEndpoint)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpFlatads parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), FLATADS_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Flatads extension: " + e.getMessage()); + } + } + + private String resolveEndpoint(ExtImpFlatads extImp) { + return endpointUrl + .replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getPublisherId()))) + .replace(TOKEN_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(extImp.getToken()))); + } + + private HttpRequest makeHttpRequest(BidRequest request, String endpoint) { + return BidderUtil.defaultRequest(request, makeHeaders(request.getDevice()), endpoint, mapper); + } + + private static MultiMap makeHeaders(Device device) { + 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()); + } + + 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(bidRequest, bidResponse, errors), errors); + } catch (DecodeException 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(); + } + return bidsFromResponse(bidRequest, bidResponse, errors); + } + + private static List bidsFromResponse(BidRequest bidRequest, + BidResponse bidResponse, + List errors) { + + final Map imps = 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, imps, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBid(Bid bid, Map imps, String currency, List errors) { + final BidType bidType = getBidType(bid.getImpid(), imps, errors); + return bidType == null ? null : BidderBid.of(bid, bidType, currency); + } + + private static BidType getBidType(String impId, Map imps, List errors) { + final Imp imp = imps.get(impId); + if (imp != null) { + if (imp.getBanner() != null) { + return BidType.banner; + } else if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getXNative() != null) { + return BidType.xNative; + } + } + + errors.add(BidderError.badServerResponse("The impression with ID %s is not present into the request" + .formatted(impId))); + return null; + } +} 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..dedd8399f1b 100644 --- a/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java +++ b/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java @@ -70,6 +70,8 @@ public class FlippBidder implements Bidder { private static final Set AD_TYPES = Set.of(4309, 641); private static final Set DTX_TYPES = Set.of(5061); private static final String EXT_REQUEST_TRANSMIT_EIDS = "transmitEids"; + private static final int DEFAULT_STANDARD_HEIGHT = 2400; + private static final int DEFAULT_COMPACT_HEIGHT = 600; private final String endpointUrl; private final JacksonMapper mapper; @@ -144,7 +146,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(List::getFirst) .orElse(null); return PrebidRequest.builder() @@ -272,32 +274,38 @@ public final Result> makeBids(BidderCall ht } } - private static List extractBids(CampaignResponseBody campaignResponseBody, BidRequest bidRequest) { + private List extractBids(CampaignResponseBody campaignResponseBody, BidRequest bidRequest) { return Optional.ofNullable(campaignResponseBody) .map(CampaignResponseBody::getDecisions) .map(Decisions::getInline) .stream() .flatMap(Collection::stream) - .filter(inline -> isInlineValid(bidRequest, inline)) - .map(inline -> BidderBid.of(constructBid(inline), BidType.banner, "USD")) + .map(inline -> makeBid(inline, getCorrespondingImp(bidRequest, inline))) + .filter(Objects::nonNull) .toList(); } - private static boolean isInlineValid(BidRequest bidRequest, Inline inline) { + private static Imp getCorrespondingImp(BidRequest bidRequest, Inline inline) { final String requestId = Optional.ofNullable(inline) .map(Inline::getPrebid) .map(Prebid::getRequestId) .orElse(null); - return requestId != null && bidRequest.getImp().stream() - .map(Imp::getId) - .anyMatch(impId -> impId.equals(requestId)); + return requestId != null + ? bidRequest.getImp().stream().filter(imp -> imp.getId().equals(requestId)).findFirst().orElse(null) + : null; } - private static Bid constructBid(Inline inline) { + private BidderBid makeBid(Inline inline, Imp imp) { + return imp == null + ? null + : BidderBid.of(constructBid(inline, parseImpExt(imp)), BidType.banner, "USD"); + } + + private static Bid constructBid(Inline inline, ExtImpFlipp extImp) { final Prebid prebid = inline.getPrebid(); final Data data = Optional.ofNullable(inline.getContents()) - .map(content -> content.get(0)) + .map(List::getFirst) .map(Content::getData) .orElse(null); @@ -308,7 +316,21 @@ private static Bid constructBid(Inline inline) { .id(Integer.toString(inline.getAdId())) .impid(prebid.getRequestId()) .w(data != null ? data.getWidth() : null) - .h(data != null ? 0 : null) + .h(resolveHeight(data, extImp)) .build(); } + + private static Integer resolveHeight(Data data, ExtImpFlipp extImp) { + final boolean startCompact = Optional.ofNullable(extImp) + .map(ExtImpFlipp::getOptions) + .map(ExtImpFlippOptions::getStartCompact) + .orElse(false); + + return Optional.ofNullable(data) + .map(Data::getCustomData) + .map(customData -> customData.get(startCompact ? "compactHeight" : "standardHeight")) + .filter(JsonNode::isNumber) + .map(JsonNode::asInt) + .orElse(startCompact ? DEFAULT_COMPACT_HEIGHT : DEFAULT_STANDARD_HEIGHT); + } } 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..f2b65b90db7 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(); } @@ -271,4 +271,3 @@ private static Bid convertBid(GammaBid gammaBid, BidType bidType) { return bid; } } - 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..62b0ad34155 100644 --- a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java @@ -34,7 +34,7 @@ public class GothamAdsBidder implements Bidder { private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; - private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String ACCOUNT_ID_MACRO = "{{AccountID}}"; private static final String X_OPENRTB_VERSION = "2.5"; private final String endpointUrl; @@ -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 { private static final String REQUEST_EXT_PRODUCT = "product"; - private static final TypeReference> GUMGUM_EXT_TYPE_REFERENCE = + private static final TypeReference> GUMGUM_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -82,17 +82,24 @@ private BidRequest createBidRequest(BidRequest bidRequest, List err for (Imp imp : bidRequest.getImp()) { try { - final ExtImpGumgum extImp = parseImpExt(imp); - modifiedImps.add(modifyImp(imp, extImp)); + final ExtPrebid extImp = parseImpExt(imp); + final ExtImpGumgum extImpGumgum = extImp.getBidder(); + final String adUnitCode = Optional.ofNullable(extImp.getPrebid()) + .map(ExtImpPrebid::getAdUnitCode) + .orElse(null); - final String extZone = extImp.getZone(); + modifiedImps.add(modifyImp(imp, extImpGumgum, adUnitCode)); + + final String extZone = extImpGumgum.getZone(); if (StringUtils.isNotEmpty(extZone)) { zone = extZone; } - final BigInteger extPubId = extImp.getPubId(); + + final BigInteger extPubId = extImpGumgum.getPubId(); if (extPubId != null && !extPubId.equals(BigInteger.ZERO)) { pubId = extPubId; } + } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -108,15 +115,15 @@ private BidRequest createBidRequest(BidRequest bidRequest, List err .build(); } - private ExtImpGumgum parseImpExt(Imp imp) { + private ExtPrebid parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), GUMGUM_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), GUMGUM_EXT_TYPE_REFERENCE); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } } - private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { + private Imp modifyImp(Imp imp, ExtImpGumgum extImp, String adUnitCode) { final Imp.ImpBuilder impBuilder = imp.toBuilder(); final String product = extImp.getProduct(); @@ -125,6 +132,10 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { impBuilder.ext(productExt); } + if (StringUtils.isNotEmpty(adUnitCode)) { + impBuilder.tagid(adUnitCode); + } + final Banner banner = imp.getBanner(); if (banner != null) { final Banner resolvedBanner = resolveBanner(banner, extImp); @@ -135,20 +146,20 @@ private Imp modifyImp(Imp imp, ExtImpGumgum extImp) { final Video video = imp.getVideo(); if (video != null) { - validateVideoParams(video); final String irisId = extImp.getIrisId(); if (StringUtils.isNotEmpty(irisId)) { final Video resolvedVideo = resolveVideo(video, irisId); impBuilder.video(resolvedVideo); } } + return impBuilder.build(); } private Banner resolveBanner(Banner banner, ExtImpGumgum extImpGumgum) { final 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,31 +185,11 @@ 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(); } - private static boolean anyOfNull(Integer... numbers) { - return Arrays.stream(ArrayUtils.nullToEmpty(numbers)).anyMatch(GumgumBidder::isNullOrZero); - } - - private static boolean isNullOrZero(Integer number) { - return number == null || number == 0; - } - private static Site modifySite(Site requestSite, String zone, BigInteger pubId) { if (requestSite == null) { return null; @@ -264,4 +255,3 @@ private static String resolveAdm(String bidAdm, BigDecimal price) { return StringUtils.isNotBlank(bidAdm) ? bidAdm.replace("${AUCTION_PRICE}", String.valueOf(price)) : bidAdm; } } - 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/HuaweiAdsBidder.java b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdsBidder.java index 1c4ad601fce..ebc2b56987e 100644 --- a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiAdsBidder.java @@ -175,9 +175,9 @@ private Regs makeRegs(com.iab.openrtb.request.Regs regs) { .orElseGet(() -> Regs.of(null)); } - private Geo makeGeo(com.iab.openrtb.request.Device device) { + private Geo makeGeo(Device device) { return Optional.ofNullable(device) - .map(com.iab.openrtb.request.Device::getGeo) + .map(Device::getGeo) .map(geo -> Geo.of(geo.getLon(), geo.getLat(), geo.getAccuracy(), geo.getLastfix())) .orElse(null); } 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/HuaweiNetworkBuilder.java b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiNetworkBuilder.java index 2ffc3ae0866..907a789c1a7 100644 --- a/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiNetworkBuilder.java +++ b/src/main/java/org/prebid/server/bidder/huaweiads/HuaweiNetworkBuilder.java @@ -1,5 +1,6 @@ package org.prebid.server.bidder.huaweiads; +import com.iab.openrtb.request.Device; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.huaweiads.model.request.CellInfo; import org.prebid.server.bidder.huaweiads.model.request.Network; @@ -11,7 +12,7 @@ public class HuaweiNetworkBuilder { private static final int DEFAULT_UNKNOWN_NETWORK_TYPE = 0; - public Network build(com.iab.openrtb.request.Device device) { + public Network build(Device device) { if (device == null) { return null; } 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..63308a8674e 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,17 @@ 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/improvedigital/proto/ImprovedigitalBidExt.java b/src/main/java/org/prebid/server/bidder/improvedigital/proto/ImprovedigitalBidExt.java index 478c2409e0b..e603a254edc 100644 --- a/src/main/java/org/prebid/server/bidder/improvedigital/proto/ImprovedigitalBidExt.java +++ b/src/main/java/org/prebid/server/bidder/improvedigital/proto/ImprovedigitalBidExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.improvedigital.proto; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ImprovedigitalBidExt { ImprovedigitalBidExtImprovedigital improvedigital; 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/insticator/InsticatorBidder.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorBidder.java new file mode 100644 index 00000000000..4a1bd8b19dd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorBidder.java @@ -0,0 +1,266 @@ +package org.prebid.server.bidder.insticator; + +import com.fasterxml.jackson.core.type.TypeReference; +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.Publisher; +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.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.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.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.insticator.ExtImpInsticator; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class InsticatorBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private static final String DEFAULT_BIDDER_CURRENCY = "USD"; + private static final String INSTICATOR_FIELD = "insticator"; + private static final InsticatorExtRequestCaller DEFAULT_INSTICATOR_CALLER = + InsticatorExtRequestCaller.of("Prebid-Server", "n/a"); + + private final CurrencyConversionService currencyConversionService; + private final String endpointUrl; + private final JacksonMapper mapper; + + public InsticatorBidder(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 Map> groupedImps = new HashMap<>(); + final List errors = new ArrayList<>(); + + String publisherId = null; + + for (Imp imp : request.getImp()) { + try { + validateImp(imp); + final ExtImpInsticator extImp = parseImpExt(imp); + + if (publisherId == null) { + publisherId = extImp.getPublisherId(); + } + + final Imp modifiedImp = modifyImp(request, imp, extImp); + groupedImps.computeIfAbsent(extImp.getAdUnitId(), key -> new ArrayList<>()).add(modifiedImp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + final BidRequest modifiedRequest = modifyRequest(request, publisherId, errors); + final List> requests = groupedImps.values().stream() + .map(imps -> modifiedRequest.toBuilder().imp(imps).build()) + .map(finalRequest -> BidderUtil.defaultRequest( + finalRequest, + makeHeaders(finalRequest.getDevice()), + endpointUrl, + mapper)) + .toList(); + + return Result.of(requests, errors); + } + + private void validateImp(Imp imp) { + final Video video = imp.getVideo(); + if (video == null) { + return; + } + + if (isInvalidDimension(video.getH()) + || isInvalidDimension(video.getW()) + || CollectionUtils.isEmpty(video.getMimes())) { + + throw new PreBidException("One or more invalid or missing video field(s) w, h, mimes"); + } + } + + private static boolean isInvalidDimension(Integer dimension) { + return dimension == null || dimension == 0; + } + + private ExtImpInsticator 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 request, Imp imp, ExtImpInsticator extImp) { + final Price bidFloorPrice = resolveBidFloor(request, imp); + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(INSTICATOR_FIELD, mapper.mapper().valueToTree(extImp))) + .bidfloorcur(bidFloorPrice.getCurrency()) + .bidfloor(bidFloorPrice.getValue()) + .build(); + } + + private Price resolveBidFloor(BidRequest bidRequest, Imp imp) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.isValidPrice(initialBidFloorPrice) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BIDDER_CURRENCY); + + return Price.of(DEFAULT_BIDDER_CURRENCY, BidderUtil.roundFloor(convertedPrice)); + } + + private BidRequest modifyRequest(BidRequest request, String publisherId, List errors) { + return request.toBuilder() + .site(modifySite(request.getSite(), publisherId)) + .app(modifyApp(request.getApp(), publisherId)) + .ext(modifyExtRequest(request.getExt(), errors)) + .build(); + } + + 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(); + } + + private ExtRequest modifyExtRequest(ExtRequest extRequest, List errors) { + final ExtRequest modifiedExtRequest = extRequest == null ? ExtRequest.empty() : extRequest; + final InsticatorExtRequest existingInsticator = getInsticatorExtRequest(modifiedExtRequest, errors); + + modifiedExtRequest.addProperty( + INSTICATOR_FIELD, + mapper.mapper().valueToTree(buildInsticator(existingInsticator))); + + return modifiedExtRequest; + } + + private InsticatorExtRequest getInsticatorExtRequest(ExtRequest modifiedExtRequest, List errors) { + try { + return mapper.mapper().convertValue( + modifiedExtRequest.getProperty(INSTICATOR_FIELD), + InsticatorExtRequest.class); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private static InsticatorExtRequest buildInsticator(InsticatorExtRequest existingInsticator) { + if (existingInsticator == null || CollectionUtils.isEmpty(existingInsticator.getCaller())) { + return InsticatorExtRequest.of(Collections.singletonList(DEFAULT_INSTICATOR_CALLER)); + } + + final List callers = new ArrayList<>(existingInsticator.getCaller()); + callers.add(DEFAULT_INSTICATOR_CALLER); + return InsticatorExtRequest.of(Collections.unmodifiableList(callers)); + } + + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, + ObjectUtil.getIfNotNull(device, Device::getUa)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIp)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, "IP", + ObjectUtil.getIfNotNull(device, Device::getIp)); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, + ObjectUtil.getIfNotNull(device, Device::getIpv6)); + + return headers; + } + + @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())) + .filter(Objects::nonNull) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 2 -> BidType.video; + case null, default -> BidType.banner; + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java new file mode 100644 index 00000000000..079c7270ea5 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.insticator; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class InsticatorExtRequest { + + List caller; + +} diff --git a/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java new file mode 100644 index 00000000000..9b7ec994558 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.insticator; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class InsticatorExtRequestCaller { + + String name; + + String version; + +} 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/invibes/InvibesBidder.java b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java index 4b00b970d09..cc4e4bfcaf8 100644 --- a/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java +++ b/src/main/java/org/prebid/server/bidder/invibes/InvibesBidder.java @@ -247,7 +247,7 @@ private static String resolveWidth(Device device) { } private static String resolveHost(Integer domainId) { - if (domainId == 0 || domainId == 1 || domainId == 1001) { + if (domainId == null || domainId == 0 || domainId == 1 || domainId == 1001) { return "bid"; } else if (domainId < 1002) { return "bid" + domainId; 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..5770772c1bd 100644 --- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java +++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java @@ -21,7 +21,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; -import org.prebid.server.bidder.ix.model.request.IxDiag; import org.prebid.server.bidder.ix.model.response.IxBidResponse; import org.prebid.server.bidder.ix.model.response.IxExtBidResponse; import org.prebid.server.bidder.ix.model.response.NativeV11Wrapper; @@ -41,9 +40,9 @@ 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.proto.openrtb.ext.response.ExtIgi; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; @@ -65,6 +64,8 @@ public class IxBidder implements Bidder { private static final TypeReference> IX_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final String PBSP_JAVA = "java"; + private static final String PBS_VERSION_UNKNOWN = "unknown"; private final String endpointUrl; private final PrebidVersionProvider prebidVersionProvider; @@ -107,21 +108,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 +152,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 +161,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,21 +171,45 @@ 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(); } - modifiedExt.addProperty("ixdiag", mapper.mapper().valueToTree(makeDiagData(extRequest, siteIds))); + modifiedExt.addProperty("ixdiag", makeDiagData(extRequest, siteIds)); return modifiedExt; } - private IxDiag makeDiagData(ExtRequest extRequest, Set siteIds) { + private ObjectNode makeDiagData(ExtRequest extRequest, Set siteIds) { final String pbjsv = Optional.ofNullable(extRequest) .map(ExtRequest::getPrebid) .map(ExtRequestPrebid::getChannel) @@ -216,31 +222,23 @@ private IxDiag makeDiagData(ExtRequest extRequest, Set siteIds) { ? siteIds.stream().sorted().collect(Collectors.joining(", ")) : null; - return IxDiag.of(pbsv, pbjsv, multipleSiteIds); - } + final ObjectNode ixdiag = Optional.ofNullable(extRequest) + .map(ext -> ext.getProperty("ixdiag")) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .orElse(mapper.mapper().createObjectNode()) + .put("pbsv", pbsv == null ? PBS_VERSION_UNKNOWN : pbsv) + .put("pbsp", PBSP_JAVA); - 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); - } + if (multipleSiteIds != null) { + ixdiag.put("multipleSiteIds", multipleSiteIds); + } - 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); - } + if (pbjsv != null) { + ixdiag.put("pbjsv", pbjsv); + } - private static Publisher modifyPublisher(Publisher publisher, String id) { - return Optional.ofNullable(publisher) - .map(Publisher::toBuilder) - .orElseGet(Publisher::builder) - .id(id) - .build(); + return ixdiag; } @Override @@ -249,14 +247,36 @@ public Result> makeBids(BidderCall httpCall, BidRequ 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 errors = new ArrayList<>(); + + return CompositeBidderResponse.builder() + .bids(extractBids(bidRequest, bidResponse, errors)) + .igi(extractIgi(bidResponse)) + .errors(errors) + .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 +291,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 +316,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,36 +352,92 @@ 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 List extractIxBids(BidRequest bidRequest, - IxBidResponse bidResponse, - List bidderErrors) { - return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) - ? Collections.emptyList() - : bidsFromResponse(bidResponse, bidRequest, bidderErrors); + 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 List extractFledge(IxBidResponse bidResponse) { - return Optional.ofNullable(bidResponse) + 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 static Response mergeNativeImpTrackers(Response response, List eventTrackers) { + final Stream impTrackers = Optional.of(response) + .map(Response::getImptrackers).stream().flatMap(Collection::stream); + + return response.toBuilder() + .imptrackers(Stream.concat( + eventTrackers.stream() + .filter(IxBidder::isImpTracker) + .map(EventTracker::getUrl), + impTrackers) + .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 extractIgi(IxBidResponse bidResponse) { + final List igs = Optional.ofNullable(bidResponse) .map(IxBidResponse::getExt) - .map(IxExtBidResponse::getFledgeAuctionConfigs) - .orElse(Collections.emptyMap()) - .entrySet() + .map(IxExtBidResponse::getProtectedAudienceAuctionConfigs) + .orElse(Collections.emptyList()) .stream() - .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build()) + .filter(Objects::nonNull) + .map(config -> ExtIgiIgs.builder().impId(config.getBidId()).config(config.getConfig()).build()) .toList(); + + return igs.isEmpty() ? null : Collections.singletonList(ExtIgi.builder().igs(igs).build()); } } diff --git a/src/main/java/org/prebid/server/bidder/ix/model/request/IxDiag.java b/src/main/java/org/prebid/server/bidder/ix/model/request/IxDiag.java deleted file mode 100644 index b3323edb849..00000000000 --- a/src/main/java/org/prebid/server/bidder/ix/model/request/IxDiag.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.bidder.ix.model.request; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -@Value(staticConstructor = "of") -public class IxDiag { - - String pbsv; - - String pbjsv; - - @JsonProperty("multipleSiteIds") - String multipleSiteIds; -} diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java new file mode 100644 index 00000000000..709fab87429 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java @@ -0,0 +1,14 @@ +package org.prebid.server.bidder.ix.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AuctionConfigExtBidResponse { + + @JsonProperty("bidId") + String bidId; + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java index c292317d22c..c586817df2c 100644 --- a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java @@ -1,15 +1,14 @@ package org.prebid.server.bidder.ix.model.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; -import java.util.Map; +import java.util.List; @Value(staticConstructor = "of") public class IxExtBidResponse { - @JsonProperty("fledge_auction_configs") - Map fledgeAuctionConfigs; + @JsonProperty("protectedAudienceAuctionConfigs") + List protectedAudienceAuctionConfigs; } diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/NativeV11Wrapper.java b/src/main/java/org/prebid/server/bidder/ix/model/response/NativeV11Wrapper.java index f4c64f411d8..7444120b2ee 100644 --- a/src/main/java/org/prebid/server/bidder/ix/model/response/NativeV11Wrapper.java +++ b/src/main/java/org/prebid/server/bidder/ix/model/response/NativeV11Wrapper.java @@ -2,14 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.iab.openrtb.response.Response; -import lombok.AllArgsConstructor; import lombok.Value; /** * Native 1.2 to 1.1 tracker compatibility handling */ -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class NativeV11Wrapper { @JsonProperty("native") 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/kobler/KoblerBidder.java b/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java new file mode 100644 index 00000000000..b87df9dcffa --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/kobler/KoblerBidder.java @@ -0,0 +1,195 @@ +package org.prebid.server.bidder.kobler; + +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.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 org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; +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.kobler.ExtImpKobler; +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 KoblerBidder implements Bidder { + + private static final TypeReference> KOBLER_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String DEFAULT_BID_CURRENCY = "USD"; + private static final String EXT_PREBID = "prebid"; + + private final String endpointUrl; + private final String devEndpoint; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public KoblerBidder(String endpointUrl, + String devEndpoint, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(endpointUrl); + this.devEndpoint = Objects.requireNonNull(devEndpoint); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final List errors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + + final List imps = bidRequest.getImp(); + for (Imp imp : imps) { + try { + modifiedImps.add(modifyImp(bidRequest, imp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return Result.withErrors(errors); + } + } + + final Device device = bidRequest.getDevice(); + final BidRequest modifiedRequest = bidRequest.toBuilder() + .imp(modifiedImps) + .cur(normalizeCurrencies(bidRequest)) + .device(device != null ? device.toBuilder().ipv6(null).ip(null).build() : null) + .user(null) + .build(); + + final String endpoint = isTest(imps.getFirst(), errors) ? devEndpoint : endpointUrl; + + final HttpRequest httpRequest = BidderUtil.defaultRequest(modifiedRequest, endpoint, mapper); + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp) { + final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest); + + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + + private List normalizeCurrencies(BidRequest bidRequest) { + final List currencies = bidRequest.getCur(); + if (currencies.contains(DEFAULT_BID_CURRENCY)) { + return currencies; + } + + final List newCurrencies = new ArrayList<>(currencies); + newCurrencies.add(DEFAULT_BID_CURRENCY); + return newCurrencies; + } + + private boolean isTest(Imp imp, List errors) { + try { + return BooleanUtils.isTrue(parseImpExt(imp).getTest()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return false; + } + } + + private ExtImpKobler parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), KOBLER_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(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 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())) + .toList(); + } + + private BidType getBidType(Bid bid) { + return Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get(EXT_PREBID)) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .filter(JsonNode::isObject) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElse(BidType.banner); + } + + 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/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/kueezrtb/KueezRtbBidder.java b/src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java new file mode 100644 index 00000000000..2cc1e3bfe5a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/kueezrtb/KueezRtbBidder.java @@ -0,0 +1,119 @@ +package org.prebid.server.bidder.kueezrtb; + +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.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.kueezrtb.KueezRtbImpExt; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; +import org.prebid.server.util.HttpUtil; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class KueezRtbBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public KueezRtbBidder(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 KueezRtbImpExt impExt = parseImpExt(imp); + requests.add(makeHttpRequest(bidRequest, imp, impExt)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return Result.of(requests, errors); + } + + private KueezRtbImpExt 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, KueezRtbImpExt 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) { + 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 | 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 { + final BidType mediaType = getMediaTypeForBid(bid); + return BidderBid.of(bid, mediaType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getMediaTypeForBid(Bid bid) { + final Integer mType = bid.getMtype(); + return switch (mType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException("Could not define bid type for imp: " + bid.getImpid()); + }; + } +} 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/mabidder/response/Meta.java b/src/main/java/org/prebid/server/bidder/mabidder/response/Meta.java index 9ba8b024ae1..7434b83c4ce 100644 --- a/src/main/java/org/prebid/server/bidder/mabidder/response/Meta.java +++ b/src/main/java/org/prebid/server/bidder/mabidder/response/Meta.java @@ -12,4 +12,3 @@ public class Meta { List adDomains; } - diff --git a/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java new file mode 100644 index 00000000000..5fdfe844c19 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/madsense/MadsenseBidder.java @@ -0,0 +1,186 @@ +package org.prebid.server.bidder.madsense; + +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.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.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.madsense.ExtImpMadsense; +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; +import java.util.Objects; + +public class MadsenseBidder implements Bidder { + + private static final String X_OPENRTB_VERSION_HEADER_VALUE = "2.6"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MadsenseBidder(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<>(); + final List videoImps = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + if (imp.getBanner() != null) { + try { + httpRequests.add(makeHttpRequest(request, Collections.singletonList(imp))); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } else if (imp.getVideo() != null) { + videoImps.add(imp); + } + } + + if (CollectionUtils.isNotEmpty(videoImps)) { + try { + httpRequests.add(makeHttpRequest(request, videoImps)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpMadsense parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext parameters"); + } + } + + private HttpRequest makeHttpRequest(BidRequest request, List imps) { + final Imp firstImp = request.getImp().getFirst(); + final ExtImpMadsense extImp = parseImpExt(firstImp); + final String companyId = Objects.equals(request.getTest(), 1) ? "test" : extImp.getCompanyId(); + return BidderUtil.defaultRequest( + request.toBuilder().imp(imps).build(), + makeHeaders(request), + makeEndpoint(companyId), + mapper); + } + + private static MultiMap makeHeaders(BidRequest request) { + final MultiMap headers = HttpUtil.headers() + .set(HttpUtil.X_OPENRTB_VERSION_HEADER, X_OPENRTB_VERSION_HEADER_VALUE); + + final Device device = request.getDevice(); + 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()); + } + + final Site site = request.getSite(); + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ORIGIN_HEADER, site.getDomain()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getRef()); + } + + return headers; + } + + private String makeEndpoint(String companyId) { + return endpointUrl + "?company_id=" + HttpUtil.encodeUrl(companyId); + } + + @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 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) + .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 = getBidType(bid); + return BidderBid.builder() + .bid(bid) + .bidCurrency(currency) + .videoInfo(bidType == BidType.video + ? ExtBidPrebidVideo.of(resolveDuration(bid), resolveCategory(bid)) + : null) + .type(bidType) + .build(); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> throw new PreBidException( + "Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid())); + }; + } + + private static String resolveCategory(Bid bid) { + final List categories = bid.getCat(); + return CollectionUtils.isEmpty(categories) ? null : categories.getFirst(); + } + + private static Integer resolveDuration(Bid bid) { + final Integer duration = bid.getDur(); + return duration != null && duration > 0 ? duration : null; + } +} 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..a82d6a72e63 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,34 @@ 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.ExtIgi; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; 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 +46,34 @@ 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<>(); + return CompositeBidderResponse.builder() + .bids(extractBids(httpCall.getRequest().getPayload(), bidResponse, errors)) + .igi(extractIgi(bidResponse)) + .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 +85,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 +135,18 @@ private static BidType resolveBidType(String impId, List imps) { return BidType.banner; } + + private static List extractIgi(MedianetBidResponse bidResponse) { + final List igs = Optional.ofNullable(bidResponse) + .map(MedianetBidResponse::getExt) + .map(MedianetBidResponseExt::getIgi) + .orElse(Collections.emptyList()) + .stream() + .map(InterestGroupAuctionIntent::getIgs) + .flatMap(Collection::stream) + .map(igiIgs -> ExtIgiIgs.builder().impId(igiIgs.getImpId()).config(igiIgs.getConfig()).build()) + .toList(); + + return igs.isEmpty() ? null : Collections.singletonList(ExtIgi.builder().igs(igs).build()); + } } 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/mediasquare/MediasquareBidder.java b/src/main/java/org/prebid/server/bidder/mediasquare/MediasquareBidder.java new file mode 100644 index 00000000000..18c9fca362b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/MediasquareBidder.java @@ -0,0 +1,296 @@ +package org.prebid.server.bidder.mediasquare; + +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.Format; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Native; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import io.vertx.core.http.HttpMethod; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.mediasquare.request.MediasquareBanner; +import org.prebid.server.bidder.mediasquare.request.MediasquareCode; +import org.prebid.server.bidder.mediasquare.request.MediasquareFloor; +import org.prebid.server.bidder.mediasquare.request.MediasquareGdpr; +import org.prebid.server.bidder.mediasquare.request.MediasquareMediaTypes; +import org.prebid.server.bidder.mediasquare.request.MediasquareRequest; +import org.prebid.server.bidder.mediasquare.request.MediasquareSupport; +import org.prebid.server.bidder.mediasquare.response.MediasquareBid; +import org.prebid.server.bidder.mediasquare.response.MediasquareResponse; +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.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.mediasquare.ExtImpMediasquare; +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.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.Optional; +import java.util.stream.Collectors; + +public class MediasquareBidder implements Bidder { + + private static final String SIZE_FORMAT = "%dx%d"; + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MediasquareBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List codes = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMediasquare extImp = parseImpExt(imp); + final MediasquareCode mediasquareCode = makeCode(request, imp, extImp); + if (isCodeValid(mediasquareCode)) { + codes.add(mediasquareCode); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (codes.isEmpty()) { + return Result.withErrors(errors); + } + + final MediasquareRequest outgoingRequest = makeRequest(request, codes); + + final HttpRequest httpRequest = HttpRequest.builder() + .method(HttpMethod.POST) + .uri(endpointUrl) + .headers(HttpUtil.headers()) + .body(mapper.encodeToBytes(outgoingRequest)) + .payload(outgoingRequest) + .impIds(BidderUtil.impIds(request)) + .build(); + + return Result.of(List.of(httpRequest), errors); + } + + private ExtImpMediasquare parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("can not parse imp.ext" + e.getMessage()); + } + } + + private static MediasquareCode makeCode(BidRequest bidRequest, Imp imp, ExtImpMediasquare extImp) { + final MediasquareMediaTypes mediaTypes = makeMediaTypes(imp); + final Map floors = mediaTypes == null + ? null + : makeFloors(MediasquareFloor.of(imp.getBidfloor(), imp.getBidfloorcur()), mediaTypes); + + return MediasquareCode.builder() + .adUnit(imp.getTagid()) + .auctionId(bidRequest.getId()) + .bidId(imp.getId()) + .code(extImp.getCode()) + .owner(extImp.getOwner()) + .mediaTypes(mediaTypes) + .floor(floors) + .build(); + } + + private static MediasquareMediaTypes makeMediaTypes(Imp imp) { + final Video video = imp.getVideo(); + final Banner banner = imp.getBanner(); + final Native xNative = imp.getXNative(); + + if (video == null && banner == null && xNative == null) { + return null; + } + + return MediasquareMediaTypes.builder() + .banner(makeBanner(banner)) + .video(video) + .nativeRequest(xNative != null ? xNative.getRequest() : null) + .build(); + } + + private static MediasquareBanner makeBanner(Banner banner) { + if (banner == null) { + return null; + } + + final List> sizes = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(banner.getFormat())) { + for (Format format : banner.getFormat()) { + sizes.add(List.of(format.getW(), format.getH())); + } + } else { + sizes.add(List.of(banner.getW(), banner.getH())); + } + + return MediasquareBanner.of(sizes); + } + + private static Map makeFloors(MediasquareFloor floor, MediasquareMediaTypes mediaTypes) { + final Map floors = new HashMap<>(); + + final Video video = mediaTypes.getVideo(); + final MediasquareBanner banner = mediaTypes.getBanner(); + final String xNative = mediaTypes.getNativeRequest(); + + if (video != null) { + if (video.getW() != null && video.getH() != null) { + final String videoSize = SIZE_FORMAT.formatted(video.getW(), video.getH()); + floors.put(videoSize, floor); + } + floors.put("*", floor); + } + + if (banner != null) { + for (List format: banner.getSizes()) { + floors.put(SIZE_FORMAT.formatted(format.get(0), format.get(1)), floor); + } + } + + if (xNative != null) { + floors.put("*", floor); + } + + return MapUtils.isNotEmpty(floors) ? floors : null; + } + + private static boolean isCodeValid(MediasquareCode code) { + final MediasquareMediaTypes mediaTypes = code.getMediaTypes(); + return mediaTypes != null && ObjectUtils.anyNotNull( + mediaTypes.getBanner(), mediaTypes.getVideo(), mediaTypes.getNativeRequest()); + } + + private MediasquareRequest makeRequest(BidRequest bidRequest, List codes) { + final User user = bidRequest.getUser(); + final Regs regs = bidRequest.getRegs(); + + return MediasquareRequest.builder() + .codes(codes) + .dsa(getDsa(regs)) + .gdpr(makeGdpr(user, regs)) + .type("pbs") + .support(MediasquareSupport.of(bidRequest.getDevice(), bidRequest.getApp())) + .test(Objects.equals(bidRequest.getTest(), 1)) + .build(); + } + + private static ExtRegsDsa getDsa(Regs regs) { + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getDsa) + .orElse(null); + } + + private static MediasquareGdpr makeGdpr(User user, Regs regs) { + final boolean gdprApplies = Optional.ofNullable(regs) + .map(Regs::getGdpr) + .map(gdpr -> gdpr == 1) + .orElse(false); + final String consent = user != null ? user.getConsent() : null; + return MediasquareGdpr.of(gdprApplies, consent); + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final MediasquareResponse response = mapper.decodeValue( + httpCall.getResponse().getBody(), + MediasquareResponse.class); + return Result.withValues(extractBids(response)); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse("Failed to decode response: " + e.getMessage())); + } + } + + private List extractBids(MediasquareResponse response) { + if (response == null || CollectionUtils.isEmpty(response.getResponses())) { + return Collections.emptyList(); + } + + return response.getResponses().stream() + .filter(Objects::nonNull) + .map(this::makeBidderBid) + .collect(Collectors.toList()); + } + + private BidderBid makeBidderBid(MediasquareBid bid) { + final BidType bidType = getBidType(bid); + return BidderBid.of(makeBid(bid, bidType), bidType, bid.getCurrency()); + } + + private static BidType getBidType(MediasquareBid bid) { + if (bid.getVideo() != null) { + return BidType.video; + } + if (bid.getNativeResponse() != null) { + return BidType.xNative; + } + return BidType.banner; + } + + private Bid makeBid(MediasquareBid bid, BidType bidType) { + return Bid.builder() + .id(bid.getId()) + .impid(bid.getBidId()) + .price(bid.getCpm()) + .adm(bid.getAd()) + .adomain(bid.getAdomain()) + .w(bid.getWidth()) + .h(bid.getHeight()) + .crid(bid.getCreativeId()) + .mtype(bidType.ordinal() + 1) + .burl(bid.getBurl()) + .ext(getBidExt(bid, bidType)) + .build(); + } + + private ObjectNode getBidExt(MediasquareBid bid, BidType bidType) { + final ExtBidPrebidMeta meta = ExtBidPrebidMeta.builder() + .advertiserDomains(bid.getAdomain() != null ? bid.getAdomain() : null) + .mediaType(bidType.getName()) + .build(); + + final ExtBidPrebid prebid = ExtBidPrebid.builder().meta(meta).build(); + + final ObjectNode bidExt = mapper.mapper().createObjectNode(); + if (bid.getDsa() != null) { + bidExt.set("dsa", bid.getDsa()); + } + bidExt.set("prebid", mapper.mapper().valueToTree(prebid)); + + return bidExt; + } +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java new file mode 100644 index 00000000000..bb8af052d81 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareBanner.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MediasquareBanner { + + List> sizes; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java new file mode 100644 index 00000000000..ea7559dcab8 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareCode.java @@ -0,0 +1,30 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareCode { + + @JsonProperty("adunit") + String adUnit; + + @JsonProperty("auctionid") + String auctionId; + + @JsonProperty("bidid") + String bidId; + + String code; + + String owner; + + @JsonProperty("mediatypes") + MediasquareMediaTypes mediaTypes; + + Map floor; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java new file mode 100644 index 00000000000..18987f20f2a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareFloor.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class MediasquareFloor { + + BigDecimal floor; + + String currency; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java new file mode 100644 index 00000000000..6dcf676a2db --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareGdpr.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.request; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediasquareGdpr { + + boolean consentRequired; + + String consentString; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java new file mode 100644 index 00000000000..da8e5ec59a7 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareMediaTypes.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.iab.openrtb.request.Video; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareMediaTypes { + + MediasquareBanner banner; + + Video video; + + String nativeRequest; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java new file mode 100644 index 00000000000..d236e55b80f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareRequest.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; + +import java.util.List; + +@Builder +@Value(staticConstructor = "of") +public class MediasquareRequest { + + List codes; + + MediasquareGdpr gdpr; + + String type; + + ExtRegsDsa dsa; + + @JsonProperty("tech") + MediasquareSupport support; + + Boolean test; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java new file mode 100644 index 00000000000..33df240a732 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/request/MediasquareSupport.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.mediasquare.request; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; +import lombok.Value; + +@Value(staticConstructor = "of") +public class MediasquareSupport { + + Device device; + + App app; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java new file mode 100644 index 00000000000..57301c8df2c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareBid.java @@ -0,0 +1,49 @@ +package org.prebid.server.bidder.mediasquare.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; +import java.util.List; + +@Value +@Builder(toBuilder = true) +public class MediasquareBid { + + String id; + + String ad; + + String bidId; + + String bidder; + + BigDecimal cpm; + + String currency; + + String creativeId; + + Integer height; + + Integer width; + + Boolean netRevenue; + + String transactionId; + + Integer ttl; + + ObjectNode video; + + @JsonProperty("native") + ObjectNode nativeResponse; + + List adomain; + + ObjectNode dsa; + + String burl; +} diff --git a/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java new file mode 100644 index 00000000000..8260fb633ee --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mediasquare/response/MediasquareResponse.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.mediasquare.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MediasquareResponse { + + List responses; +} 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..7e57d054fab --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/metax/MetaxBidder.java @@ -0,0 +1,176 @@ +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.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.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; +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("0"); + final String adUnitAsString = Optional.ofNullable(extImpMetax.getAdUnit()) + .map(Object::toString) + .orElse("0"); + + 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.builder() + .bid(bid) + .type(getBidType(bid)) + .bidCurrency(bidResponse.getCur()) + .videoInfo(videoInfo(bid)) + .build()) + .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())); + }; + } + + 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/mgid/MgidBidder.java b/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java index 180b99d39ff..637a122bad2 100644 --- a/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java +++ b/src/main/java/org/prebid/server/bidder/mgid/MgidBidder.java @@ -104,7 +104,7 @@ private static String getCur(ExtImpMgid impMgid) { } private static String currencyValueOrNull(String value) { - return StringUtils.isNotBlank(value) && !value.equals("USD") ? value : null; + return StringUtils.isNotBlank(value) && !"USD".equals(value) ? value : null; } private static BigDecimal getBidFloor(ExtImpMgid impMgid) { @@ -169,4 +169,3 @@ private ExtBidMgid getBidExt(Bid bid) { } } } - diff --git a/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java b/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java index bcc9f57c099..48563ed6ab9 100644 --- a/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java +++ b/src/main/java/org/prebid/server/bidder/mgid/model/ExtBidMgid.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.mgid.model; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.proto.openrtb.ext.response.BidType; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtBidMgid { BidType crtype; 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..c5b6663315a 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; @@ -109,14 +108,14 @@ 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); - final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse); + final List bids = extractBids(bidResponse); return Result.withValues(bids); } 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(); } diff --git a/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java b/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java index 544e4df12d8..e1ade2c251a 100644 --- a/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java +++ b/src/main/java/org/prebid/server/bidder/minutemedia/MinuteMediaBidder.java @@ -37,10 +37,12 @@ public class MinuteMediaBidder implements Bidder { public static final String PUBLISHER_ID_MACRO = "{{PublisherId}}"; private final String endpointUrl; + private final String testEndpointUrl; private final JacksonMapper mapper; - public MinuteMediaBidder(String endpointUrl, JacksonMapper mapper) { + public MinuteMediaBidder(String endpointUrl, String testEndpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.testEndpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(testEndpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @@ -49,15 +51,15 @@ public Result>> makeHttpRequests(BidRequest bidRequ final String orgId; try { - orgId = extractFirstImpOrdId(bidRequest.getImp()); + orgId = extractFirstImpOrgId(bidRequest.getImp()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } - return Result.withValue(BidderUtil.defaultRequest(bidRequest, resolveEndpoint(endpointUrl, orgId), mapper)); + return Result.withValue(BidderUtil.defaultRequest(bidRequest, makeUrl(orgId, bidRequest.getTest()), mapper)); } - private String extractFirstImpOrdId(List imps) { + private String extractFirstImpOrgId(List imps) { return imps.stream() .findFirst() .map(this::parseImpExt) @@ -76,8 +78,9 @@ private ExtImpMinuteMedia parseImpExt(Imp imp) { } } - private String resolveEndpoint(String endpointUrl, String orgId) { - return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(orgId)); + private String makeUrl(String orgId, Integer test) { + final String url = Objects.equals(test, 1) ? testEndpointUrl : endpointUrl; + return url.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(orgId)); } @Override @@ -120,15 +123,11 @@ private static BidderBid makeBidderBid(Bid bid, String currency, List BidType.banner; case 2 -> BidType.video; - default -> throw new PreBidException( - "Unsupported bid mediaType: %s for impression: %s".formatted(bid.getMtype(), bid.getImpid())); + case null, default -> throw new PreBidException( + "Unsupported bid mediaType: %s for impression: %s".formatted(markupType, bid.getImpid())); }; } } 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..2f9722b2c82 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaAdRequest.java @@ -0,0 +1,36 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.BidRequest; +import lombok.Builder; +import lombok.Value; + +import java.math.BigDecimal; + +@Value +@Builder(toBuilder = true) +public class MissenaAdRequest { + + @JsonProperty("adunit") + String adUnit; + + String currency; + + BigDecimal floor; + + String floorCurrency; + + @JsonProperty("ik") + String idempotencyKey; + + String requestId; + + Long timeout; + + MissenaUserParams params; + + @JsonProperty("ortb2") + BidRequest bidRequest; + + String version; +} 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..eacc3a49541 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaBidder.java @@ -0,0 +1,209 @@ +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.Site; +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.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.missena.ExtImpMissena; +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.version.PrebidVersionProvider; + +import java.math.BigDecimal; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class MissenaBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String USD_CURRENCY = "USD"; + private static final String EUR_CURRENCY = "EUR"; + private static final String PUBLISHER_ID_MACRO = "{{PublisherID}}"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + private final PrebidVersionProvider prebidVersionProvider; + + public MissenaBidder(String endpointUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final ExtImpMissena extImp = parseImpExt(imp); + final HttpRequest httpRequest = makeHttpRequest(request, imp, extImp); + return Result.of(Collections.singletonList(httpRequest), errors); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + return Result.withErrors(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, Imp imp, ExtImpMissena extImp) { + final Site site = request.getSite(); + final Device device = request.getDevice(); + + final String requestCurrency = resolveCurrency(request.getCur()); + final Price floorInfo = resolveBidFloor(imp, request, requestCurrency); + + final MissenaUserParams userParams = MissenaUserParams.builder() + .formats(extImp.getFormats()) + .placement(extImp.getPlacement()) + .testMode(extImp.getTestMode()) + .settings(extImp.getSettings()) + .build(); + + final MissenaAdRequest missenaAdRequest = MissenaAdRequest.builder() + .adUnit(imp.getId()) + .currency(requestCurrency) + .floor(floorInfo.getValue()) + .floorCurrency(floorInfo.getCurrency()) + .idempotencyKey(request.getId()) + .requestId(request.getId()) + .timeout(request.getTmax()) + .params(userParams) + .version(prebidVersionProvider.getNameVersionRecord()) + .bidRequest(request) + .build(); + + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(resolveEndpointUrl(extImp.getApiKey())) + .headers(makeHeaders(device, site)) + .impIds(Collections.singleton(imp.getId())) + .body(mapper.encodeToBytes(missenaAdRequest)) + .payload(missenaAdRequest) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest, String targetCurrency) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.isValidPrice(initialBidFloorPrice) + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest, targetCurrency) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest, String targetCurrency) { + final String bidFloorCur = bidFloorPrice.getCurrency(); + + try { + final BigDecimal convertedPrice = currencyConversionService + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, targetCurrency); + + return Price.of(targetCurrency, convertedPrice); + } catch (PreBidException e) { + throw new PreBidException("Unable to convert provided bid floor currency from %s to %s for imp `%s`" + .formatted(bidFloorCur, targetCurrency, impId)); + } + } + + private String resolveCurrency(List requestCurrencies) { + String currency = USD_CURRENCY; + + for (String requestCurrency : requestCurrencies) { + if (USD_CURRENCY.equalsIgnoreCase(requestCurrency)) { + return USD_CURRENCY; + } + + if (EUR_CURRENCY.equalsIgnoreCase(requestCurrency)) { + currency = EUR_CURRENCY; + } + } + + return currency; + } + + 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 && StringUtils.isNotBlank(site.getPage())) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); + try { + final URL url = new URL(site.getPage()); + final String origin = url.getProtocol() + "://" + url.getHost(); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ORIGIN_HEADER, origin); + } catch (MalformedURLException e) { + // do nothing + } + } + return headers; + } + + private String resolveEndpointUrl(String apiKey) { + return endpointUrl.replace(PUBLISHER_ID_MACRO, HttpUtil.encodeUrl(apiKey)); + } + + @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/missena/MissenaUserParams.java b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java new file mode 100644 index 00000000000..e63a704a60c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/missena/MissenaUserParams.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; // Changed import +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class MissenaUserParams { + + List formats; + + String placement; + + @JsonProperty("test") + String testMode; + + ObjectNode settings; +} + 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/mobilefuse/MobilefuseBidder.java b/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java index c0e4dc9e292..40e5cf72074 100644 --- a/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java +++ b/src/main/java/org/prebid/server/bidder/mobilefuse/MobilefuseBidder.java @@ -23,6 +23,7 @@ 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; @@ -45,64 +46,62 @@ public MobilefuseBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final String endpoint = request.getImp().stream() - .map(this::parseImpExt) - .filter(Objects::nonNull) - .findFirst() - .map(this::makeUrl) - .orElse(null); - - if (endpoint == null) { - return Result.withError(BidderError.badInput("Invalid ExtImpMobilefuse value")); + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + if (!isValidImp(imp)) { + continue; + } + + final ExtImpMobilefuse extImp = parseImpExt(imp); + modifiedImps.add(modifyImp(imp, extImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } } - final List modifiedImps = request.getImp().stream() - .map(this::modifyImp) - .filter(Objects::nonNull) - .toList(); + if (!errors.isEmpty()) { + return Result.withErrors(errors); + } if (modifiedImps.isEmpty()) { return Result.withError(BidderError.badInput("No valid imps")); } final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).build(); - return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, endpoint, mapper)); + return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper)); } - private Imp modifyImp(Imp imp) { - if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { - return null; - } - - final ExtImpMobilefuse impExt = parseImpExt(imp); - final ObjectNode skadn = parseSkadn(imp.getExt()); - return imp.toBuilder() - .tagid(Objects.toString(impExt.getPlacementId(), "0")) - .ext(skadn != null ? mapper.mapper().createObjectNode().set(SKADN_PROPERTY_NAME, skadn) : null) - .build(); + private static boolean isValidImp(Imp imp) { + return imp.getBanner() != null || imp.getVideo() != null || imp.getXNative() != null; } private ExtImpMobilefuse parseImpExt(Imp imp) { try { return mapper.mapper().convertValue(imp.getExt(), MOBILEFUSE_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - return null; + throw new PreBidException("Error parsing ExtImpMobilefuse value: %s".formatted(e.getMessage())); } } + private Imp modifyImp(Imp imp, ExtImpMobilefuse extImp) { + final ObjectNode skadn = parseSkadn(imp.getExt()); + return imp.toBuilder() + .tagid(Objects.toString(extImp.getPlacementId(), "0")) + .ext(skadn != null ? mapper.mapper().createObjectNode().set(SKADN_PROPERTY_NAME, skadn) : null) + .build(); + } + private ObjectNode parseSkadn(ObjectNode impExt) { try { return mapper.mapper().convertValue(impExt.get(SKADN_PROPERTY_NAME), ObjectNode.class); } catch (IllegalArgumentException e) { - return null; + throw new PreBidException(e.getMessage()); } } - private String makeUrl(ExtImpMobilefuse extImp) { - final String baseUrl = endpointUrl + Objects.toString(extImp.getPublisherId(), "0"); - return "ext".equals(extImp.getTagidSrc()) ? baseUrl + "&tagid_src=ext" : baseUrl; - } - @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { diff --git a/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java b/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java new file mode 100644 index 00000000000..dbf82da1b4f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/mobkoi/MobkoiBidder.java @@ -0,0 +1,137 @@ +package org.prebid.server.bidder.mobkoi; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; +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.ExtUser; +import org.prebid.server.proto.openrtb.ext.request.mobkoi.ExtImpMobkoi; +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 MobkoiBidder implements Bidder { + + private static final TypeReference> MOBKOI_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public MobkoiBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Result>> makeHttpRequests(BidRequest bidRequest) { + final Imp firstImp = bidRequest.getImp().getFirst(); + + final ExtImpMobkoi extImpMobkoi; + final Imp modifiedFirstImp; + try { + extImpMobkoi = parseExtImp(firstImp); + modifiedFirstImp = modifyImp(firstImp, extImpMobkoi); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + return Result.withValue(BidderUtil.defaultRequest( + modifyBidRequest(bidRequest, modifiedFirstImp), + endpointUrl, + mapper)); + } + + private ExtImpMobkoi parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), MOBKOI_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "Invalid imp.ext for impression id %s. Error Information: %s" + .formatted(imp.getId(), e.getMessage())); + } + } + + private Imp modifyImp(Imp firstImp, ExtImpMobkoi extImpMobkoi) { + if (StringUtils.isNotBlank(firstImp.getTagid())) { + return firstImp; + } + + if (StringUtils.isNotBlank(extImpMobkoi.getPlacementId())) { + return firstImp.toBuilder().tagid(extImpMobkoi.getPlacementId()).build(); + } + + throw new PreBidException("invalid because it comes with neither request.imp[0].tagId nor " + + "req.imp[0].ext.Bidder.placementId"); + } + + private static BidRequest modifyBidRequest(BidRequest bidRequest, Imp modifiedFirstImp) { + final User user = modifyUser(bidRequest.getUser()); + final List imps = updateFirstImpWith(bidRequest.getImp(), modifiedFirstImp); + return bidRequest.toBuilder().user(user).imp(imps).build(); + } + + private static User modifyUser(User user) { + return Optional.ofNullable(user) + .map(User::getConsent) + .map(consent -> ExtUser.builder().consent(consent).build()) + .map(ext -> user.toBuilder().ext(ext).build()) + .orElse(user); + } + + private static List updateFirstImpWith(List imps, Imp imp) { + final List modifiedImps = new ArrayList<>(imps); + modifiedImps.set(0, imp); + return Collections.unmodifiableList(modifiedImps); + } + + @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.banner, "mobkoi", bidResponse.getCur())) + .toList(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/model/BidderBid.java b/src/main/java/org/prebid/server/bidder/model/BidderBid.java index ef4b18eaa2d..559d072cdd4 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderBid.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderBid.java @@ -19,6 +19,8 @@ public class BidderBid { */ Bid bid; + String seat; + /** * Will become response.seatbid[i].bid.ext.prebid.type in the final OpenRTB response. */ @@ -52,4 +54,13 @@ public static BidderBid of(Bid bid, BidType bidType, String bidCurrency) { .bidCurrency(bidCurrency) .build(); } + + public static BidderBid of(Bid bid, BidType bidType, String seat, String bidCurrency) { + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(bidCurrency) + .seat(seat) + .build(); + } } diff --git a/src/main/java/org/prebid/server/bidder/model/BidderCall.java b/src/main/java/org/prebid/server/bidder/model/BidderCall.java index 4254801a6e4..a62a6c466c5 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderCall.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderCall.java @@ -5,7 +5,7 @@ import lombok.Value; /** - * Packages together the fields needed to make an http request. + * Packages together the fields needed to make a http request. */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/org/prebid/server/bidder/model/BidderCallType.java b/src/main/java/org/prebid/server/bidder/model/BidderCallType.java index 8b5b5b0603c..72a77b1fca1 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderCallType.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderCallType.java @@ -10,4 +10,3 @@ public enum BidderCallType { @JsonProperty("stored-bid-response-call") STORED_BID_RESPONSE } - 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..9475a13e1de 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderError.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderError.java @@ -77,9 +77,9 @@ public enum Type { /** * Covers the case where a bidder failed to generate any http requests to get bids, but did not generate any - * error messages. This should not happen in practice and will signal that an bidder is poorly coded. - * If there was something wrong with a request such that an bidder could not generate a bid, then it should - * generate an error explaining the deficiency. Otherwise it will be extremely difficult to debug the reason + * error messages. This should not happen in practice and will signal that a bidder is poorly coded. + * If there was something wrong with a request such that a bidder could not generate a bid, then it should + * generate an error explaining the deficiency. Otherwise, it will be extremely difficult to debug the reason * why a bidder is not bidding. */ failed_to_request_bids(4), @@ -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/BidderSeatBid.java b/src/main/java/org/prebid/server/bidder/model/BidderSeatBid.java index 6cdc55cccb9..8022b8667f1 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderSeatBid.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderSeatBid.java @@ -4,6 +4,7 @@ import lombok.Value; import org.prebid.server.bidder.Bidder; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.util.Collections; @@ -53,9 +54,13 @@ public class BidderSeatBid { @Builder.Default List warnings = Collections.emptyList(); + @Deprecated @Builder.Default List fledgeAuctionConfigs = Collections.emptyList(); + @Builder.Default + List igi = Collections.emptyList(); + public BidderSeatBid with(List bids) { return toBuilder().bids(bids).build(); } diff --git a/src/main/java/org/prebid/server/bidder/model/BidderSeatBidInfo.java b/src/main/java/org/prebid/server/bidder/model/BidderSeatBidInfo.java index 37ff7cccfc2..38c9f86433c 100644 --- a/src/main/java/org/prebid/server/bidder/model/BidderSeatBidInfo.java +++ b/src/main/java/org/prebid/server/bidder/model/BidderSeatBidInfo.java @@ -1,15 +1,14 @@ package org.prebid.server.bidder.model; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.auction.model.BidInfo; import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class BidderSeatBidInfo { List bidsInfos; @@ -20,9 +19,18 @@ public class BidderSeatBidInfo { List warnings; + @Deprecated(forRemoval = true) List fledgeAuctionConfigs; + List igi; + public BidderSeatBidInfo with(List bids) { - return BidderSeatBidInfo.of(bids, this.httpCalls, this.errors, this.warnings, this.fledgeAuctionConfigs); + return BidderSeatBidInfo.of( + bids, + this.httpCalls, + this.errors, + this.warnings, + this.fledgeAuctionConfigs, + this.igi); } } diff --git a/src/main/java/org/prebid/server/bidder/model/CompositeBidderResponse.java b/src/main/java/org/prebid/server/bidder/model/CompositeBidderResponse.java index 7e5a31f16b0..75a71e0864a 100644 --- a/src/main/java/org/prebid/server/bidder/model/CompositeBidderResponse.java +++ b/src/main/java/org/prebid/server/bidder/model/CompositeBidderResponse.java @@ -3,6 +3,7 @@ import lombok.Builder; import lombok.Value; import org.prebid.server.bidder.Bidder; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import java.util.Collections; @@ -26,19 +27,12 @@ public class CompositeBidderResponse { */ List fledgeAuctionConfigs; + List igi; + public static CompositeBidderResponse empty() { return builder().build(); } - public static CompositeBidderResponse withBids(List bids, - List fledgeAuctionConfigs) { - - return builder() - .bids(bids) - .fledgeAuctionConfigs(fledgeAuctionConfigs) - .build(); - } - public static CompositeBidderResponse withError(BidderError error) { return builder().errors(Collections.singletonList(error)).build(); } diff --git a/src/main/java/org/prebid/server/bidder/model/HttpRequest.java b/src/main/java/org/prebid/server/bidder/model/HttpRequest.java index d8ae76eeae9..872675e88d3 100644 --- a/src/main/java/org/prebid/server/bidder/model/HttpRequest.java +++ b/src/main/java/org/prebid/server/bidder/model/HttpRequest.java @@ -8,7 +8,7 @@ import java.util.Set; /** - * Packages together the fields needed to make an http request. + * Packages together the fields needed to make a http request. */ @Builder(toBuilder = true) @Value diff --git a/src/main/java/org/prebid/server/bidder/model/HttpResponse.java b/src/main/java/org/prebid/server/bidder/model/HttpResponse.java index 3635b9c87c6..b502429cb7b 100644 --- a/src/main/java/org/prebid/server/bidder/model/HttpResponse.java +++ b/src/main/java/org/prebid/server/bidder/model/HttpResponse.java @@ -1,14 +1,12 @@ package org.prebid.server.bidder.model; import io.vertx.core.MultiMap; -import lombok.AllArgsConstructor; import lombok.Value; /** * Packages together information from the server's http response. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HttpResponse { int statusCode; diff --git a/src/main/java/org/prebid/server/bidder/model/ImpWithExt.java b/src/main/java/org/prebid/server/bidder/model/ImpWithExt.java deleted file mode 100644 index f62f435375c..00000000000 --- a/src/main/java/org/prebid/server/bidder/model/ImpWithExt.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.bidder.model; - -import com.iab.openrtb.request.Imp; -import lombok.Value; - -@Value(staticConstructor = "of") -public class ImpWithExt { - - Imp imp; - - T impExt; -} 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/model/Result.java b/src/main/java/org/prebid/server/bidder/model/Result.java index 1f1ae2ef216..a5ce54ecb75 100644 --- a/src/main/java/org/prebid/server/bidder/model/Result.java +++ b/src/main/java/org/prebid/server/bidder/model/Result.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Collections; @@ -9,8 +8,7 @@ /** * Defines generic result that might bear error alongside. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class Result { T value; 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/nativery/NativeryBidder.java b/src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java new file mode 100644 index 00000000000..ef333e4d95c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java @@ -0,0 +1,246 @@ +package org.prebid.server.bidder.nativery; + +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.collections4.ListUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.Endpoint; +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.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; +import org.prebid.server.proto.openrtb.ext.request.nativery.BidExtNativery; +import org.prebid.server.proto.openrtb.ext.request.nativery.ExtImpNativery; +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.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class NativeryBidder implements Bidder { + + private static final TypeReference> NATIVERY_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String DEFAULT_CURRENCY = "EUR"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public NativeryBidder(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<>(); + + final String requestEndpointName = extractEndpointName(request); + final boolean isAmp = StringUtils.equals(requestEndpointName, Endpoint.openrtb2_amp.value()); + + final List validImps = new ArrayList<>(); + String widgetId = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpNativery extImp = parseImpExt(imp); + if (widgetId == null && StringUtils.isNotBlank(extImp.getWidgetId())) { + widgetId = extImp.getWidgetId(); + } + validImps.add(imp); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (validImps.isEmpty()) { + return Result.withErrors(errors); + } + + final ExtRequest updatedExt = buildRequestExtWithNativery(request.getExt(), isAmp, widgetId); + + for (Imp imp : validImps) { + final BidRequest singleImpRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .ext(updatedExt) + .build(); + + httpRequests.add(BidderUtil.defaultRequest(singleImpRequest, endpointUrl, mapper)); + } + + return Result.of(httpRequests, errors); + } + + private static String extractEndpointName(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getServer) + .map(ExtRequestPrebidServer::getEndpoint) + .orElse(null); + } + + private ExtImpNativery parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Nativery extension: " + e.getMessage()); + } + } + + private ExtRequest buildRequestExtWithNativery(ExtRequest originalExt, boolean isAmp, String widgetId) { + final ExtRequest ext = originalExt != null ? originalExt : ExtRequest.empty(); + + final JsonNode existing = ext.getProperty("nativery"); + final ObjectNode nativeryNode = existing != null && existing.isObject() + ? (ObjectNode) existing + : mapper.mapper().createObjectNode(); + + nativeryNode.put("isAmp", isAmp); + if (StringUtils.isNotBlank(widgetId)) { + nativeryNode.put("widgetId", widgetId); + } + + ext.addProperty("nativery", nativeryNode); + return ext; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); + + final HttpResponse response = httpCall.getResponse(); + + try { + final BidResponse bidResponse = mapper.decodeValue(response.getBody(), BidResponse.class); + 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(); + } + + final String currency = StringUtils.defaultIfBlank(bidResponse.getCur(), DEFAULT_CURRENCY); + + return bidResponse.getSeatbid().stream() + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> resolveBidderBid(bid, currency, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid resolveBidderBid(Bid bid, String currency, List errors) { + try { + final BidExtNativery nativeryExt = parseNativeryExt(bid.getExt()); + + final BidType bidType = mapMediaType(nativeryExt.getBidAdMediaType()); + final List advDomains = ListUtils.emptyIfNull( + nativeryExt.getBidAdvDomains()); + + final Bid updatedBid = addBidMeta(bid, bidType.getName(), advDomains); + return BidderBid.of(updatedBid, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + return null; + } + } + + private BidExtNativery parseNativeryExt(ObjectNode bidExt) { + return Optional.ofNullable(bidExt) + .map(ext -> ext.get("nativery")) + .filter(JsonNode::isObject) + .map(this::toBidExtNativery) + .orElseThrow(() -> new PreBidException("missing bid.ext.nativery")); + } + + private BidExtNativery toBidExtNativery(JsonNode node) { + try { + return mapper.mapper().convertValue(node, BidExtNativery.class); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage()); + } + } + + private static BidType mapMediaType(String mediaType) { + final String mt = StringUtils.defaultString(mediaType).toLowerCase(); + return switch (mt) { + case "native" -> BidType.xNative; + case "display", "banner", "rich_media" -> BidType.banner; + case "video" -> BidType.video; + default -> throw new PreBidException( + "unrecognized bid_ad_media_type in response from nativery: " + mediaType); + }; + } + + private Bid addBidMeta(Bid bid, String mediaType, List advDomains) { + final ExtBidPrebid prebid = parseExtBidPrebid(bid); + + final ExtBidPrebidMeta modifiedMeta = Optional.ofNullable(prebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .mediaType(mediaType) + .advertiserDomains(advDomains) + .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 mapper.mapper() + .convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE) + .getPrebid(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Failed to deserialize Prebid extension: " + e.getMessage()); + } + } +} 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..57f5200f786 100644 --- a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java @@ -8,10 +8,10 @@ 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; -import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -21,21 +21,28 @@ 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.nextmillennium.proto.NextMillenniumExt; +import org.prebid.server.bidder.nextmillennium.proto.NextMillenniumExtBidder; +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.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.nextmillennium.ExtImpNextMillennium; 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 org.prebid.server.version.PrebidVersionProvider; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; public class NextMillenniumBidder implements Bidder { @@ -43,61 +50,67 @@ public class NextMillenniumBidder implements Bidder { new TypeReference<>() { }; + private static final String NM_ADAPTER_VERSION = "v1.0.1"; + private final String endpointUrl; private final JacksonMapper mapper; private final List nmmFlags; + private final PrebidVersionProvider versionProvider; + + public NextMillenniumBidder(String endpointUrl, + JacksonMapper mapper, + List nmmFlags, + PrebidVersionProvider versionProvider) { - public NextMillenniumBidder(String endpointUrl, JacksonMapper mapper, List nmmFlags) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); this.mapper = Objects.requireNonNull(mapper); this.nmmFlags = nmmFlags; - + this.versionProvider = Objects.requireNonNull(versionProvider); } @Override public final Result>> 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; + } + final BidRequest updatedRequest = updateBidRequest(bidRequest, extImpNextMillennium); + httpRequests.add(BidderUtil.defaultRequest( + updatedRequest, + headers(), + endpointUrl, + mapper)); + } - 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))) - .build(); - final ExtRequest extRequest = ExtRequest.of(prebid); - - final List imps = bidRequest.getImp().stream() - .map(imp -> imp.toBuilder().ext(createImpExt(prebid)).build()) - .toList(); + private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpNextMillennium extImp) { + final String storedRequestId = resolveStoredRequestId(bidRequest, extImp); + final ExtRequestPrebidServer extRequestPrebidServer = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getServer) + .orElse(null); - return bidRequest.toBuilder().imp(imps).ext(extRequest).build(); + return bidRequest.toBuilder() + .imp(modifyFirstImp(bidRequest.getImp(), storedRequestId)) + .ext(createExtRequest(storedRequestId, extRequestPrebidServer, extImp)) + .build(); } private static String resolveStoredRequestId(BidRequest bidRequest, ExtImpNextMillennium extImpNextMillennium) { @@ -106,7 +119,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 +134,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( @@ -137,24 +150,45 @@ private static String formatSize(Integer w, Integer h) { : null; } - private ObjectNode createImpExt(ExtRequestPrebid prebid) { - final ObjectNode impExt = mapper.mapper().createObjectNode(); - impExt.set("prebid", mapper.mapper().valueToTree(prebid)); - if (CollectionUtils.isNotEmpty(nmmFlags)) { - impExt.putObject("nextMillennium") - .set("nmmFlags", mapper.mapper().valueToTree(nmmFlags)); - } - return impExt; + private List modifyFirstImp(List imps, String storedRequestId) { + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of(storedRequestId)) + .build(); + + final NextMillenniumExt nextMillenniumExt = NextMillenniumExt.of( + NextMillenniumExtBidder.of(nmmFlags)); + + final ExtRequest extRequest = ExtRequest.of(extRequestPrebid); + mapper.fillExtension(extRequest, nextMillenniumExt); + + final ObjectNode impExt = mapper.mapper().valueToTree(extRequest); + + final List modifiedImps = new ArrayList<>(imps); + modifiedImps.set(0, imps.getFirst().toBuilder().ext(impExt).build()); + + return modifiedImps; } - private HttpRequest makeHttpRequest(BidRequest bidRequest) { - return HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(headers()) - .payload(bidRequest) - .body(mapper.encodeToBytes(bidRequest)) + private ExtRequest createExtRequest(String storedRequestId, + ExtRequestPrebidServer extRequestPrebidServer, + ExtImpNextMillennium extImp) { + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(ExtStoredRequest.of(storedRequestId)) + .server(extRequestPrebidServer) .build(); + + final NextMillenniumExt nextMillenniumExt = NextMillenniumExt.of( + NextMillenniumExtBidder.of( + nmmFlags, + extImp.getAdSlots(), + extImp.getAllowedAds(), + NM_ADAPTER_VERSION, + versionProvider.getNameVersionRecord())); + + final ExtRequest extRequest = ExtRequest.of(extRequestPrebid); + mapper.fillExtension(extRequest, nextMillenniumExt); + + return extRequest; } private static MultiMap headers() { @@ -164,24 +198,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/nextmillennium/proto/NextMillenniumExt.java b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java new file mode 100644 index 00000000000..b5b8f58f6c2 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.nextmillennium.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class NextMillenniumExt { + + @JsonProperty("nextMillennium") + NextMillenniumExtBidder nextMillennium; +} diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java new file mode 100644 index 00000000000..910896950ca --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/proto/NextMillenniumExtBidder.java @@ -0,0 +1,27 @@ +package org.prebid.server.bidder.nextmillennium.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class NextMillenniumExtBidder { + + @JsonProperty("nmmFlags") + List nmmFlags; + + @JsonProperty("adSlots") + List adSlots; + + @JsonProperty("allowedAds") + List allowedAds; + + String nmVersion; + + String serverVersion; + + public static NextMillenniumExtBidder of(List nmmFlags) { + return of(nmmFlags, null, null, null, null); + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java new file mode 100644 index 00000000000..180f545bf47 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360Bidder.java @@ -0,0 +1,176 @@ +package org.prebid.server.bidder.nexx360; + +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.apache.http.client.utils.URIBuilder; +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.nexx360.ExtImpNexx360; +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.version.PrebidVersionProvider; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Nexx360Bidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String BIDDER_NAME = "nexx360"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final PrebidVersionProvider prebidVersionProvider; + + public Nexx360Bidder(String endpointUrl, JacksonMapper mapper, PrebidVersionProvider prebidVersionProvider) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List imps = request.getImp(); + final List modifiedImps = new ArrayList<>(); + + final ExtImpNexx360 firstExtImp; + try { + firstExtImp = parseImpExt(imps.getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + + for (final Imp imp : imps) { + modifiedImps.add(modifyImp(imp)); + } + + final BidRequest modifiedRequest = makeRequest(request, modifiedImps); + final String url = makeUrl(firstExtImp.getTagId(), firstExtImp.getPlacement()); + return Result.withValue(BidderUtil.defaultRequest(modifiedRequest, url, mapper)); + } + + private ExtImpNexx360 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) { + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set(BIDDER_NAME, imp.getExt().get("bidder"))) + .build(); + } + + private BidRequest makeRequest(BidRequest request, List imps) { + final ExtRequest extRequest = ExtRequest.empty(); + extRequest.addProperty(BIDDER_NAME, mapper.mapper().valueToTree( + Nexx360ExtRequest.of(Nexx360ExtRequestCaller.of(prebidVersionProvider.getNameVersionRecord())))); + + return request.toBuilder() + .imp(imps) + .ext(extRequest) + .build(); + } + + private String makeUrl(String tagId, String placement) { + final URIBuilder uriBuilder; + try { + uriBuilder = new URIBuilder(endpointUrl); + } catch (URISyntaxException e) { + throw new PreBidException("Invalid url: %s, error: %s".formatted(endpointUrl, e.getMessage())); + } + + if (StringUtils.isNotBlank(placement)) { + uriBuilder.addParameter("placement", placement); + } + if (StringUtils.isNotBlank(tagId)) { + uriBuilder.addParameter("tag_id", tagId); + } + + return uriBuilder.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 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) + .filter(Objects::nonNull) + .map(bid -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private 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 BidType getBidType(Bid bid) { + final String bidType; + try { + bidType = mapper.mapper() + .convertValue(bid.getExt(), Nexx360ExtBid.class) + .getBidType(); + } catch (IllegalArgumentException e) { + throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + } + + return switch (bidType) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + case "audio" -> BidType.audio; + case "native" -> BidType.xNative; + default -> throw new PreBidException( + "unable to fetch mediaType in multi-format: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java new file mode 100644 index 00000000000..3e4ecaf4cda --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtBid.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtBid { + + @JsonProperty("bidType") + String bidType; +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java new file mode 100644 index 00000000000..1ff6bc3ead1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +import java.util.Collections; +import java.util.List; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequest { + + List caller; + + public static Nexx360ExtRequest of(Nexx360ExtRequestCaller caller) { + return of(Collections.singletonList(caller)); + } +} diff --git a/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java new file mode 100644 index 00000000000..72da1af310a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/nexx360/Nexx360ExtRequestCaller.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.nexx360; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Nexx360ExtRequestCaller { + + String name; + + String version; + + public static Nexx360ExtRequestCaller of(String version) { + return Nexx360ExtRequestCaller.of("Prebid-Server", version); + } +} diff --git a/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java new file mode 100644 index 00000000000..4d14198e230 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ogury/OguryBidder.java @@ -0,0 +1,230 @@ +package org.prebid.server.bidder.ogury; + +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; +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.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.JacksonMapper; +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 OguryBidder implements Bidder { + + private static final String EXT_FIELD_BIDDER = "bidder"; + private static final String BIDDER_CURRENCY = "USD"; + private static final String PREBID_FIELD_ASSET_KEY = "assetKey"; + private static final String PREBID_FIELD_ADUNIT_ID = "adUnitId"; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + + public OguryBidder(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 modifiedImps = new ArrayList<>(); + final List impsWithOguryParams = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + try { + final Imp modifiedImp = modifyImp(imp, bidRequest); + + modifiedImps.add(modifiedImp); + if (hasOguryParams(imp)) { + impsWithOguryParams.add(modifiedImp); + } + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (CollectionUtils.isEmpty(impsWithOguryParams)) { + // we can serve ads with just publisher.id + if (!hasPublisherId(bidRequest)) { + errors.add(BidderError.badInput( + "Invalid request. assetKey/adUnitId or request.site/app.publisher.id required")); + return Result.withErrors(errors); + } + } + + final BidRequest modifiedBidRequest = bidRequest.toBuilder() + .imp(CollectionUtils.isNotEmpty(impsWithOguryParams) ? impsWithOguryParams : modifiedImps) + .build(); + + final MultiMap headers = resolveHeaders(modifiedBidRequest.getDevice()); + final List> httpRequests = Collections.singletonList( + BidderUtil.defaultRequest(modifiedBidRequest, headers, endpointUrl, mapper)); + + return Result.of(httpRequests, errors); + } + + private ObjectNode resolveImpExtBidderHoist(Imp imp) { + return (ObjectNode) imp.getExt().get(EXT_FIELD_BIDDER); + } + + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + final Price price = resolvePrice(imp, bidRequest); + return imp.toBuilder() + .tagid(imp.getId()) + .bidfloor(price.getValue()) + .bidfloorcur(price.getCurrency()) + .ext(modifyExt(imp)) + .build(); + } + + private Price resolvePrice(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } + + private ObjectNode modifyExt(Imp imp) { + final ObjectNode impExt = imp.getExt(); + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(imp); + + final ObjectNode modifiedImpExt = impExt.deepCopy(); + modifiedImpExt.setAll(impExtBidderHoist); + modifiedImpExt.remove(EXT_FIELD_BIDDER); + + return modifiedImpExt; + } + + private boolean hasOguryParams(Imp imp) { + final ObjectNode impExtBidderHoist = resolveImpExtBidderHoist(imp); + + return impExtBidderHoist != null + && impExtBidderHoist.has(PREBID_FIELD_ASSET_KEY) + && impExtBidderHoist.has(PREBID_FIELD_ADUNIT_ID); + } + + private boolean hasPublisherId(BidRequest request) { + return hasSitePublisherId(request) || hasAppPublisherId(request); + } + + private boolean hasSitePublisherId(BidRequest request) { + return Optional.ofNullable(request.getSite()) + .map(Site::getPublisher) + .map(Publisher::getId) + .isPresent(); + } + + private boolean hasAppPublisherId(BidRequest request) { + return Optional.ofNullable(request.getApp()) + .map(App::getPublisher) + .map(Publisher::getId) + .isPresent(); + } + + private 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()); + } + + return headers; + } + + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + try { + final String body = httpCall.getResponse().getBody(); + + final BidResponse bidResponse = mapper.decodeValue(body, BidResponse.class); + + final List errors = new ArrayList<>(); + final List bidderBids = extractBids(bidResponse, errors); + + return Result.of(bidderBids, errors); + } catch (Exception e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + } + + private List extractBids(BidResponse bidResponse, List errors) { + return Optional.ofNullable(bidResponse) + .map(BidResponse::getSeatbid) + .stream() + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> createBidderBid(bid, bidResponse, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private BidderBid createBidderBid(Bid bid, BidResponse bidResponse, List errors) { + try { + return BidderBid.of(bid, getBidType(bid), bidResponse.getCur()); + } 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 impression: `%s`".formatted(bid.getImpid())); + } + + 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', for impression '%s'".formatted(markupType, bid.getImpid())); + }; + } +} 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..81e1c8091f5 100644 --- a/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java +++ b/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java @@ -1,19 +1,27 @@ package org.prebid.server.bidder.oms; +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.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.omx.ExtImpOms; 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; @@ -24,6 +32,8 @@ public class OmsBidder implements Bidder { + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; private final String endpointUrl; private final JacksonMapper mapper; @@ -33,42 +43,83 @@ 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()); + public Result>> makeHttpRequests(BidRequest request) { + try { + final ExtImpOms impExt = parseImpExt(request.getImp().getFirst()); + final String publisherId = resolverPublisherId(impExt.getPid(), impExt.getPublisherId()); + final String encodedPublisherId = HttpUtil.encodeUrl(publisherId); + final String url = "%s?publisherId=%s".formatted(endpointUrl, encodedPublisherId); + return Result.withValue(BidderUtil.defaultRequest(request, url, mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpOms parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid ext. Imp.Id: " + imp.getId()); + } + } + + private String resolverPublisherId(String pid, Integer publisherId) { + if (StringUtils.isEmpty(pid) && publisherId != null && publisherId > 0) { + return String.valueOf(publisherId); + } + return pid; } @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) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> createBidderBid(bid, bidResponse.getCur())) .toList(); } + + private static BidderBid createBidderBid(Bid bid, String currency) { + final BidType bidType = getBidType(bid); + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(currency) + .videoInfo(videoInfo(bidType, bid)) + .build(); + } + + private static BidType getBidType(Bid bid) { + return Objects.equals(bid.getMtype(), 2) ? BidType.video : BidType.banner; + } + + private static ExtBidPrebidVideo videoInfo(BidType bidType, Bid bid) { + if (bidType != BidType.video) { + return null; + } + final List cat = bid.getCat(); + final Integer duration = bid.getDur(); + + return ExtBidPrebidVideo.of( + ObjectUtils.defaultIfNull(duration, 0), + CollectionUtils.isNotEmpty(cat) ? cat.getFirst() : StringUtils.EMPTY); + } } diff --git a/src/main/java/org/prebid/server/bidder/onetag/OnetagBidder.java b/src/main/java/org/prebid/server/bidder/onetag/OnetagBidder.java index c7fa5589fb2..ec6c9718ea5 100644 --- a/src/main/java/org/prebid/server/bidder/onetag/OnetagBidder.java +++ b/src/main/java/org/prebid/server/bidder/onetag/OnetagBidder.java @@ -22,7 +22,6 @@ 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; @@ -45,20 +44,18 @@ public OnetagBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final List modifiedImps = new ArrayList<>(); String requestPubId = null; for (Imp imp : request.getImp()) { try { final ExtImpOnetag impExt = parseImpExt(imp); requestPubId = resolveAndValidatePubId(impExt.getPubId(), requestPubId); - - modifiedImps.add(imp.toBuilder().ext(impExt.getExt()).build()); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } } - return Result.withValue(createRequest(request, modifiedImps, requestPubId)); + final String url = endpointUrl.replace(URL_PUBLISHER_ID_MACRO, StringUtils.defaultString(requestPubId)); + return Result.withValue(BidderUtil.defaultRequest(request, url, mapper)); } private ExtImpOnetag parseImpExt(Imp imp) { @@ -69,8 +66,8 @@ private ExtImpOnetag parseImpExt(Imp imp) { } } - private String resolveAndValidatePubId(String impExtPubId, String requestPubId) { - if (StringUtils.isEmpty(impExtPubId)) { + private static String resolveAndValidatePubId(String impExtPubId, String requestPubId) { + if (StringUtils.isBlank(impExtPubId)) { throw new PreBidException("The publisher ID must not be empty"); } if (requestPubId != null && !impExtPubId.equals(requestPubId)) { @@ -79,13 +76,6 @@ private String resolveAndValidatePubId(String impExtPubId, String requestPubId) return impExtPubId; } - private HttpRequest createRequest(BidRequest request, List imps, String pubId) { - final String url = endpointUrl.replace(URL_PUBLISHER_ID_MACRO, pubId); - final BidRequest outgoingRequest = request.toBuilder().imp(imps).build(); - - return BidderUtil.defaultRequest(outgoingRequest, url, mapper); - } - @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { 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..00a8c8dc3fb 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,8 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.openx.model.OpenxImpType; +import org.prebid.server.bidder.openx.proto.OpenxBidExt; +import org.prebid.server.bidder.openx.proto.OpenxBidResponse; import org.prebid.server.bidder.openx.proto.OpenxBidResponseExt; import org.prebid.server.bidder.openx.proto.OpenxRequestExt; import org.prebid.server.bidder.openx.proto.OpenxVideoExt; @@ -29,7 +30,11 @@ 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.FledgeAuctionConfig; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -43,6 +48,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; public class OpenxBidder implements Bidder { @@ -71,9 +77,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ .collect(Collectors.groupingBy(OpenxBidder::resolveImpType)); final List processingErrors = new ArrayList<>(); - final List outgoingRequests = makeRequests(bidRequest, + final List outgoingRequests = makeRequests( + bidRequest, differentiatedImps.get(OpenxImpType.banner), - differentiatedImps.get(OpenxImpType.video), processingErrors); + differentiatedImps.get(OpenxImpType.video), + differentiatedImps.get(OpenxImpType.xNative), + processingErrors); final List errors = errors(differentiatedImps.get(OpenxImpType.other), processingErrors); @@ -83,9 +92,13 @@ public Result>> makeHttpRequests(BidRequest bidRequ @Override public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - final OpenxBidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), - OpenxBidResponse.class); - return CompositeBidderResponse.withBids(extractBids(bidRequest, bidResponse), extractFledge(bidResponse)); + final OpenxBidResponse bidResponse = mapper.decodeValue( + httpCall.getResponse().getBody(), OpenxBidResponse.class); + + return CompositeBidderResponse.builder() + .bids(extractBids(bidRequest, bidResponse)) + .igi(extractIgi(bidResponse)) + .build(); } catch (DecodeException e) { return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } @@ -100,13 +113,21 @@ public Result> makeBids(BidderCall httpCall, BidRequ return Result.withError(BidderError.generic("Deprecated adapter method invoked")); } - private List makeRequests(BidRequest bidRequest, List bannerImps, List videoImps, - List errors) { + private List makeRequests( + BidRequest bidRequest, + List bannerImps, + List videoImps, + List nativeImps, + List errors) { final List bidRequests = new ArrayList<>(); - // single request for all banner imps - final BidRequest bannerRequest = createSingleRequest(bannerImps, bidRequest, errors); - if (bannerRequest != null) { - bidRequests.add(bannerRequest); + // single request for all banner and native imps + final List bannerAndNativeImps = Stream.of(bannerImps, nativeImps) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .toList(); + final BidRequest bannerAndNativeImpsRequest = createSingleRequest(bannerAndNativeImps, bidRequest, errors); + if (bannerAndNativeImpsRequest != null) { + bidRequests.add(bannerAndNativeImpsRequest); } if (CollectionUtils.isNotEmpty(videoImps)) { @@ -127,16 +148,33 @@ private static OpenxImpType resolveImpType(Imp imp) { if (imp.getVideo() != null) { return OpenxImpType.video; } + if (imp.getXNative() != null) { + return OpenxImpType.xNative; + } return OpenxImpType.other; } + private static BidType resolveBidType(Imp imp) { + if (imp.getBanner() != null) { + return BidType.banner; + } + if (imp.getVideo() != null) { + return BidType.video; + } + if (imp.getXNative() != null) { + return BidType.xNative; + } + return BidType.banner; + } + private List errors(List notSupportedImps, List processingErrors) { final List errors = new ArrayList<>(); // add errors for imps with unsupported media types if (CollectionUtils.isNotEmpty(notSupportedImps)) { errors.addAll( notSupportedImps.stream() - .map(imp -> "OpenX only supports banner and video imps. Ignoring imp id=" + imp.getId()) + .map(imp -> + "OpenX only supports banner, video and native imps. Ignoring imp id=" + imp.getId()) .map(BidderError::badInput) .toList()); } @@ -169,7 +207,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; } @@ -235,13 +273,13 @@ private ObjectNode makeImpExt(ObjectNode impExt, boolean addCustomParams) { return openxImpExt; } - private static List extractBids(BidRequest bidRequest, OpenxBidResponse bidResponse) { + private List extractBids(BidRequest bidRequest, OpenxBidResponse bidResponse) { return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, OpenxBidResponse bidResponse) { + private List bidsFromResponse(BidRequest bidRequest, OpenxBidResponse bidResponse) { final Map impIdToBidType = impIdToBidType(bidRequest); final String bidCurrency = StringUtils.isNotBlank(bidResponse.getCur()) @@ -253,27 +291,94 @@ 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 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.toBuilder().ext(getBidExt(bid)).build()) + .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)); + .collect(Collectors.toMap(Imp::getId, OpenxBidder::resolveBidType)); } private static BidType getBidType(Bid bid, Map impIdToBidType) { - return impIdToBidType.getOrDefault(bid.getImpid(), BidType.banner); + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> impIdToBidType.getOrDefault(bid.getImpid(), BidType.banner); + }; } - private static List extractFledge(OpenxBidResponse bidResponse) { - return Optional.ofNullable(bidResponse) + private static List extractIgi(OpenxBidResponse bidResponse) { + final List igs = Optional.ofNullable(bidResponse) .map(OpenxBidResponse::getExt) .map(OpenxBidResponseExt::getFledgeAuctionConfigs) .orElse(Collections.emptyMap()) .entrySet() .stream() - .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build()) + .map(ext -> ExtIgiIgs.builder().impId(ext.getKey()).config(ext.getValue()).build()) .toList(); + + return igs.isEmpty() ? null : Collections.singletonList(ExtIgi.builder().igs(igs).build()); + } + + private ObjectNode getBidExt(Bid bid) { + final ObjectNode ext = bid.getExt(); + if (ext == null) { + return null; + } + + final OpenxBidExt openxBidExt = parseOpenxBidExt(ext); + final Integer buyerId = parseStringToInt(openxBidExt.getBuyerId()); + final Integer dspId = parseStringToInt(openxBidExt.getDspId()); + final Integer brandId = parseStringToInt(openxBidExt.getBrandId()); + + if (buyerId == null && dspId == null && brandId == null) { + return ext; + } + + final ExtBidPrebidMeta meta = ExtBidPrebidMeta.builder() + .networkId(dspId) + .advertiserId(buyerId) + .brandId(brandId) + .build(); + + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().meta(meta).build(); + + ext.set(PREBID_EXT, mapper.mapper().valueToTree(extBidPrebid)); + + return ext; + } + + private OpenxBidExt parseOpenxBidExt(ObjectNode ext) { + try { + return mapper.mapper().convertValue(ext, OpenxBidExt.class); + } catch (IllegalArgumentException e) { + return OpenxBidExt.builder().build(); + } + } + + private static Integer parseStringToInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return null; + } } } diff --git a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java index 7d9dfb4e5d5..c872e7f97e6 100644 --- a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java +++ b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java @@ -3,7 +3,7 @@ public enum OpenxImpType { // supported - banner, video, + banner, video, xNative, // not supported other } diff --git a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java new file mode 100644 index 00000000000..00305cfb64b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.openx.proto; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class OpenxBidExt { + + String dspId; + + String buyerId; + + String brandId; +} diff --git a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxRequestExt.java b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxRequestExt.java index 680c60ecda3..ebaa9594f91 100644 --- a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxRequestExt.java +++ b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxRequestExt.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.openx.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class OpenxRequestExt { @JsonProperty("delDomain") diff --git a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxVideoExt.java b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxVideoExt.java index fe2e6afcf7f..390020eca71 100644 --- a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxVideoExt.java +++ b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxVideoExt.java @@ -1,12 +1,8 @@ package org.prebid.server.bidder.openx.proto; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value -@Builder +@Value(staticConstructor = "of") public class OpenxVideoExt { Integer rewarded; 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..ab93ea6d4fb 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()) @@ -243,4 +243,3 @@ private BidType parseBidType(String bidType) { } } } - diff --git a/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java b/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java new file mode 100644 index 00000000000..8e6e3572fea --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/optidigital/OptidigitalBidder.java @@ -0,0 +1,66 @@ +package org.prebid.server.bidder.optidigital; + +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 OptidigitalBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public OptidigitalBidder(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 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.banner, bidResponse.getCur())) + .toList(); + } + +} 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..26bfecbe1a1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/oraki/OrakiBidder.java @@ -0,0 +1,137 @@ +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..226d956c8e1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/ownadx/OwnAdxBidder.java @@ -0,0 +1,139 @@ +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(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) + .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/pangle/model/BidExt.java b/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java index ded899b2091..d2701ca3f51 100644 --- a/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java +++ b/src/main/java/org/prebid/server/bidder/pangle/model/BidExt.java @@ -1,13 +1,9 @@ package org.prebid.server.bidder.pangle.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Builder(toBuilder = true) -@Value +@Value(staticConstructor = "of") public class BidExt { @JsonProperty("adtype") diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java b/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java index ae59d88b9f4..6f80493eb38 100644 --- a/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java +++ b/src/main/java/org/prebid/server/bidder/pangle/model/NetworkIds.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.pangle.model; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class NetworkIds { String appid; diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java b/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java index c85dd19b087..d40bd00abd2 100644 --- a/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java +++ b/src/main/java/org/prebid/server/bidder/pangle/model/PangleBidExt.java @@ -1,12 +1,8 @@ package org.prebid.server.bidder.pangle.model; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Builder(toBuilder = true) -@Value +@Value(staticConstructor = "of") public class PangleBidExt { BidExt pangle; diff --git a/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java b/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java index 10db06f4819..7b570484b4c 100644 --- a/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java +++ b/src/main/java/org/prebid/server/bidder/pangle/model/WrappedImpExtBidder.java @@ -1,13 +1,11 @@ package org.prebid.server.bidder.pangle.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.pangle.ExtImpPangle; -@AllArgsConstructor(staticName = "of") @Builder(toBuilder = true) @Value public class WrappedImpExtBidder { diff --git a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java index ce0a5161120..a73438b3ce3 100644 --- a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java +++ b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java @@ -14,15 +14,19 @@ 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.pgamssp.PgamSspImpExt; 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.Collections; import java.util.List; @@ -35,12 +39,18 @@ public class PgamSspBidder implements Bidder { }; private static final String PUBLISHER_IMP_EXT_TYPE = "publisher"; private static final String NETWORK_IMP_EXT_TYPE = "network"; + private static final String DEFAULT_BID_CURRENCY = "USD"; private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public PgamSspBidder(String endpointUrl, JacksonMapper mapper) { + public PgamSspBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -51,7 +61,7 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { final PgamSspImpExt impExt = parseImpExt(imp); - final BidRequest modifiedBidRequest = makeRequest(request, imp, impExt); + final BidRequest modifiedBidRequest = makeRequest(request, modifyImp(imp, request), impExt); httpRequests.add(makeHttpRequest(modifiedBidRequest, imp.getId())); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); @@ -61,6 +71,31 @@ public Result>> makeHttpRequests(BidRequest request return Result.withValues(httpRequests); } + private Imp modifyImp(Imp imp, BidRequest bidRequest) { + final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest); + return imp.toBuilder() + .bidfloor(resolvedBidFloor.getValue()) + .bidfloorcur(resolvedBidFloor.getCurrency()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + DEFAULT_BID_CURRENCY); + + return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); + } + private PgamSspImpExt parseImpExt(Imp imp) throws PreBidException { try { return mapper.mapper().convertValue(imp.getExt(), PGAMSSP_EXT_TYPE_REFERENCE).getBidder(); 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..eec8b56f980 --- /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(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 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..7b8be79fa7d 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java @@ -13,9 +13,12 @@ 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.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.auction.aliases.AlternateBidder; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -25,6 +28,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt; import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer; +import org.prebid.server.bidder.pubmatic.model.request.PubmaticMarketplace; import org.prebid.server.bidder.pubmatic.model.request.PubmaticWrapper; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt; import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidResponse; @@ -33,27 +37,35 @@ 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.FlexibleExtension; +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; 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.ExtBidPrebidMeta; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; -import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; +import org.prebid.server.proto.openrtb.ext.response.ExtIgi; +import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; 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.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; public class PubmaticBidder implements Bidder { @@ -63,14 +75,19 @@ public class PubmaticBidder implements Bidder { private static final String IMP_EXT_AD_UNIT_KEY = "dfp_ad_unit_code"; private static final String AD_SERVER_GAM = "gam"; private static final String PREBID = "prebid"; + private static final String MARKETPLACE_EXT_REQUEST = "marketplace"; private static final String ACAT_EXT_REQUEST = "acat"; 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 SKADN = "skadn"; 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); private static final String DCTR_VALUE_FORMAT = "%s=%s"; + private static final String WILDCARD = "*"; + private static final String WILDCARD_ALL = "all"; private final String endpointUrl; private final JacksonMapper mapper; @@ -88,9 +105,15 @@ public Result>> makeHttpRequests(BidRequest request String publisherId = null; PubmaticWrapper wrapper; final List acat; + final Pair displayManagerFields; + final List allowedBidders; + try { - acat = extractAcat(request); - wrapper = extractWrapper(request); + final JsonNode bidderparams = getExtRequestPrebidBidderparams(request); + acat = extractAcat(bidderparams); + wrapper = extractWrapper(bidderparams); + allowedBidders = extractAllowedBidders(request); + displayManagerFields = extractDisplayManagerFields(request.getApp()); } catch (IllegalArgumentException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -102,12 +125,13 @@ 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()); - validImps.add(modifyImp(imp, impExt)); + validImps.add(modifyImp(imp, impExt, displayManagerFields.getLeft(), displayManagerFields.getRight())); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -117,12 +141,43 @@ public Result>> makeHttpRequests(BidRequest request return Result.withErrors(errors); } - final BidRequest modifiedBidRequest = modifyBidRequest(request, validImps, publisherId, wrapper, acat); + final BidRequest modifiedBidRequest = modifyBidRequest( + request, validImps, publisherId, wrapper, acat, allowedBidders); return Result.of(Collections.singletonList(makeHttpRequest(modifiedBidRequest)), errors); } - private List extractAcat(BidRequest request) { - final JsonNode bidderParams = getExtRequestPrebidBidderparams(request); + private List extractAllowedBidders(BidRequest request) { + final AlternateBidderCodesConfig alternateBidderCodes = Optional.ofNullable(request.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAlternateBidderCodes) + .orElse(null); + + if (alternateBidderCodes == null) { + return null; + } + + if (BooleanUtils.isNotTrue(alternateBidderCodes.getEnabled())) { + return Collections.singletonList(BIDDER_NAME); + } + + final AlternateBidder alternateBidder = Optional.ofNullable(alternateBidderCodes.getBidders()) + .map(bidders -> bidders.get(BIDDER_NAME)) + .filter(bidder -> BooleanUtils.isTrue(bidder.getEnabled())) + .orElse(null); + + if (alternateBidder == null) { + return Collections.singletonList(BIDDER_NAME); + } + + final Set allowedBidderCodes = alternateBidder.getAllowedBidderCodes(); + if (allowedBidderCodes == null || allowedBidderCodes.contains(WILDCARD)) { + return Collections.singletonList(WILDCARD_ALL); + } + + return Stream.concat(Stream.of(BIDDER_NAME), allowedBidderCodes.stream()).toList(); + } + + private List extractAcat(JsonNode bidderParams) { final JsonNode acatNode = bidderParams != null ? bidderParams.get(ACAT_EXT_REQUEST) : null; return acatNode != null && acatNode.isArray() @@ -132,9 +187,8 @@ private List extractAcat(BidRequest request) { : null; } - private PubmaticWrapper extractWrapper(BidRequest request) { - final JsonNode pubmatic = getExtRequestPrebidBidderparams(request); - final JsonNode wrapperNode = pubmatic != null ? pubmatic.get(WRAPPER_EXT_REQUEST) : null; + private PubmaticWrapper extractWrapper(JsonNode bidderParams) { + final JsonNode wrapperNode = bidderParams != null ? bidderParams.get(WRAPPER_EXT_REQUEST) : null; return wrapperNode != null && wrapperNode.isObject() ? mapper.mapper().convertValue(wrapperNode, PubmaticWrapper.class) @@ -148,6 +202,34 @@ private static JsonNode getExtRequestPrebidBidderparams(BidRequest request) { return bidderParams != null ? bidderParams.get(BIDDER_NAME) : null; } + private Pair extractDisplayManagerFields(App app) { + String source; + String version; + + final ExtApp extApp = app != null ? app.getExt() : null; + final ExtAppPrebid extAppPrebid = extApp != null ? extApp.getPrebid() : null; + + source = extAppPrebid != null ? extAppPrebid.getSource() : null; + version = extAppPrebid != null ? extAppPrebid.getVersion() : null; + if (StringUtils.isNoneBlank(source, version)) { + return Pair.of(source, version); + } + + source = getPropertyValue(extApp, "source"); + version = getPropertyValue(extApp, "version"); + return StringUtils.isNoneBlank(source, version) + ? Pair.of(source, version) + : Pair.of(null, null); + } + + private static String getPropertyValue(FlexibleExtension flexibleExtension, String propertyName) { + return Optional.ofNullable(flexibleExtension) + .map(ext -> ext.getProperty(propertyName)) + .filter(JsonNode::isValueNode) + .map(JsonNode::asText) + .orElse(null); + } + private static void validateMediaType(Imp imp) { if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { throw new PreBidException( @@ -156,6 +238,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 +270,39 @@ 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) { + private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt, String displayManager, String displayManagerVersion) { 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); + .displaymanager(StringUtils.firstNonBlank(imp.getDisplaymanager(), displayManager)) + .displaymanagerver(StringUtils.firstNonBlank(imp.getDisplaymanagerver(), displayManagerVersion)) + .ext(!newExt.isEmpty() ? newExt : null); + + enrichWithAdSlotParameters(impBuilder, impExtBidder.getAdSlot(), banner); - return enrichWithAdSlotParameters(impBuilder, impExtBidder.getAdSlot(), banner).build(); + 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 +313,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 +323,102 @@ 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)); - } + private ObjectNode makeKeywords(PubmaticBidderImpExt impExt) { + final ObjectNode keywordsNode = mapper.mapper().createObjectNode(); - impBuilder.tagid(adSlotParams[0]); + final ExtImpPubmatic extBidder = impExt.getBidder(); + putExtBidderKeywords(keywordsNode, extBidder); + putExtDataKeywords(keywordsNode, impExt.getData(), extBidder.getDctr()); - final String[] adSize = adSlotParams[1].toLowerCase().split("x"); - if (adSize.length != 2) { - throw new PreBidException("Invalid size provided in adSlot '%s'".formatted(trimmedAdSlot)); + if (impExt.getAe() != null) { + keywordsNode.put(AE, impExt.getAe()); } - - final Integer width = parseAdSizeParam(adSize[0], "width", adSlot); - final String[] heightParams = adSize[1].split(":"); - final Integer height = parseAdSizeParam(heightParams[0], "height", adSlot); - - return 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)); + if (impExt.getGpId() != null) { + keywordsNode.put(GP_ID, impExt.getGpId()); } - } - - 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.getSkadn() != null) { + keywordsNode.set(SKADN, impExt.getSkadn()); } - 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,40 +429,66 @@ 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, List imps, String publisherId, PubmaticWrapper wrapper, - List acat) { + List acat, + List allowedBidders) { return request.toBuilder() .imp(imps) - .app(modifyApp(request.getApp(), publisherId)) .site(modifySite(request.getSite(), publisherId)) - .ext(modifyExtRequest(request.getExt(), wrapper, acat)) + .app(modifyApp(request.getApp(), publisherId)) + .ext(modifyExtRequest(wrapper, acat, allowedBidders)) .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 +511,31 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId : Publisher.builder().id(publisherId).build(); } + private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List acat, List allowedBidders) { + 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); + } + + if (allowedBidders != null) { + extNode.putPOJO(MARKETPLACE_EXT_REQUEST, PubmaticMarketplace.of(allowedBidders)); + } + + final ExtRequest newExtRequest = ExtRequest.empty(); + return extNode.isEmpty() + ? newExtRequest + : mapper.fillExtension(newExtRequest, 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 */ @@ -432,11 +548,15 @@ public Result> makeBids(BidderCall httpCall, BidRequ @Override public CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { try { - final List bidderErrors = new ArrayList<>(); final PubmaticBidResponse bidResponse = mapper.decodeValue( - httpCall.getResponse().getBody(), - PubmaticBidResponse.class); - return CompositeBidderResponse.withBids(extractBids(bidResponse, bidderErrors), extractFledge(bidResponse)); + httpCall.getResponse().getBody(), PubmaticBidResponse.class); + final List errors = new ArrayList<>(); + + return CompositeBidderResponse.builder() + .bids(extractBids(bidResponse, errors)) + .igi(extractIgi(bidResponse)) + .errors(errors) + .build(); } catch (DecodeException | PreBidException e) { return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } @@ -455,55 +575,63 @@ private List bidsFromResponse(PubmaticBidResponse bidResponse, List resolveBidderBid(bid, bidResponse.getCur(), bidderErrors)) + .filter(Objects::nonNull) .toList(); } private BidderBid resolveBidderBid(Bid bid, String currency, 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 = parseBidExt(bid.getExt(), bidderErrors); + final BidType bidType = getBidType(bid, bidderErrors); + + if (bidType == null) { + return null; + } - final PubmaticBidExt pubmaticBidExt = extractBidExt(bid.getExt()); - 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 - ? bid.toBuilder() + + final Bid updatedBid = bid.toBuilder() + .cat(firstCat) .adm(resolvedAdm != null ? resolvedAdm : bidAdm) - .cat(singleElementBidCat) - .ext(duration != null ? updateBidExtWithExtPrebid(duration, bid.getExt()) : bid.getExt()) - .build() - : bid; + .ext(updateBidExtWithExtPrebid(pubmaticBidExt, bidType, bid.getExt())) + .build(); return BidderBid.builder() .bid(updatedBid) .type(bidType) .bidCurrency(currency) .dealPriority(getDealPriority(pubmaticBidExt)) + .seat(pubmaticBidExt == null ? null : pubmaticBidExt.getMarketplace()) .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 BidType getBidType(PubmaticBidExt bidExt) { - final Integer bidType = bidExt != null - ? ObjectUtils.defaultIfNull(bidExt.getBidType(), 0) - : 0; - - return switch (bidType) { - case 1 -> BidType.video; - case 2 -> BidType.xNative; - default -> BidType.banner; + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse("failed to parse bid mtype (%d) for impression id %s" + .formatted(bid.getMtype(), bid.getImpid()))); + yield null; + } }; } @@ -517,21 +645,40 @@ private String resolveNativeAdm(String adm, List bidderErrors) { } final JsonNode nativeNode = admNode.get("native"); - if (!nativeNode.isMissingNode()) { + if (nativeNode != null && !nativeNode.isMissingNode()) { return nativeNode.toString(); } return null; } + private ObjectNode updateBidExtWithExtPrebid(PubmaticBidExt pubmaticBidExt, BidType type, ObjectNode extBid) { + final Integer duration = getDuration(pubmaticBidExt); + final boolean inBannerVideo = getInBannerVideo(pubmaticBidExt); + + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() + .video(duration != null ? ExtBidPrebidVideo.of(duration, null) : null) + .meta(ExtBidPrebidMeta.builder() + .mediaType(inBannerVideo ? BidType.video.getName() : type.getName()) + .build()) + .build(); + + return extBid != null + ? extBid.set(PREBID, mapper.mapper().valueToTree(extBidPrebid)) + : mapper.mapper().createObjectNode().set(PREBID, mapper.mapper().valueToTree(extBidPrebid)); + } + private static Integer getDuration(PubmaticBidExt bidExt) { - final VideoCreativeInfo creativeInfo = bidExt != null ? bidExt.getVideo() : null; - return creativeInfo != null ? creativeInfo.getDuration() : null; + return Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getVideo) + .map(VideoCreativeInfo::getDuration) + .orElse(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)); + private static boolean getInBannerVideo(PubmaticBidExt bidExt) { + return Optional.ofNullable(bidExt) + .map(PubmaticBidExt::getInBannerVideo) + .orElse(false); } private static Integer getDealPriority(PubmaticBidExt bidExt) { @@ -540,14 +687,16 @@ private static Integer getDealPriority(PubmaticBidExt bidExt) { .orElse(null); } - private static List extractFledge(PubmaticBidResponse bidResponse) { - return Optional.ofNullable(bidResponse) + private static List extractIgi(PubmaticBidResponse bidResponse) { + final List igs = Optional.ofNullable(bidResponse) .map(PubmaticBidResponse::getExt) .map(PubmaticExtBidResponse::getFledgeAuctionConfigs) .orElse(Collections.emptyMap()) .entrySet() .stream() - .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build()) + .map(config -> ExtIgiIgs.builder().impId(config.getKey()).config(config.getValue()).build()) .toList(); + + return igs.isEmpty() ? null : Collections.singletonList(ExtIgi.builder().igs(igs).build()); } } 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..0a248ff8a5b 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,8 @@ public class PubmaticBidderImpExt { Integer ae; + @JsonProperty("gpid") + String gpId; + + ObjectNode skadn; } diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java index b112cbe9e95..b1068e1e171 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticExtDataAdServer.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.pubmatic.model.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class PubmaticExtDataAdServer { String name; diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java new file mode 100644 index 00000000000..8c4f593dbd3 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/request/PubmaticMarketplace.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.pubmatic.model.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class PubmaticMarketplace { + + @JsonProperty("allowedbidders") + List allowedBidders; +} diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java index b469bbd59b6..a368a98cc68 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/PubmaticBidExt.java @@ -1,20 +1,18 @@ package org.prebid.server.bidder.pubmatic.model.response; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class PubmaticBidExt { - @JsonProperty("BidType") - @JsonAlias({"bidtype", "bidType"}) - Integer bidType; - VideoCreativeInfo video; @JsonProperty("prebiddealpriority") Integer prebidDealPriority; + + String marketplace; + + @JsonProperty("ibv") + Boolean inBannerVideo; } diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java index 7b2254200e9..4f961fb5e29 100644 --- a/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java +++ b/src/main/java/org/prebid/server/bidder/pubmatic/model/response/VideoCreativeInfo.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.pubmatic.model.response; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class VideoCreativeInfo { Integer duration; 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/rediads/RediadsBidder.java b/src/main/java/org/prebid/server/bidder/rediads/RediadsBidder.java new file mode 100644 index 00000000000..c0dad6f8760 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/rediads/RediadsBidder.java @@ -0,0 +1,169 @@ +package org.prebid.server.bidder.rediads; + +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.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.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.rediads.ExtImpRediads; +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.stream.Collectors; + +public class RediadsBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference<>() { + }; + private static final String SUBDOMAIN_MACRO = "{{SUBDOMAIN}}"; + + private final String endpointUrl; + private final String defaultSubdomain; + private final JacksonMapper mapper; + + public RediadsBidder(String endpointUrl, JacksonMapper mapper, String defaultSubdomain) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + this.defaultSubdomain = Objects.requireNonNull(defaultSubdomain); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + String accountId = null; + String endpoint = null; + + for (Imp imp : request.getImp()) { + try { + final ExtImpRediads extImp = parseImpExt(imp); + modifiedImps.add(modifyImp(imp, extImp)); + accountId = extImp.getAccountId(); + endpoint = extImp.getEndpoint(); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.isEmpty()) { + return Result.withErrors(errors); + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, accountId); + final String endpointUrl = resolveEndpointUrl(endpoint); + final HttpRequest httpRequest = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private ExtImpRediads parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid imp.ext for impression " + imp.getId()); + } + } + + private Imp modifyImp(Imp imp, ExtImpRediads extImp) { + final ObjectNode modifiedExt = imp.getExt().deepCopy(); + modifiedExt.remove("bidder"); + modifiedExt.remove("prebid"); + return imp.toBuilder() + .tagid(StringUtils.defaultIfBlank(extImp.getSlot(), imp.getTagid())) + .ext(modifiedExt) + .build(); + } + + private BidRequest modifyRequest(BidRequest request, List imps, String accountId) { + final Site site = request.getSite(); + final App app = request.getApp(); + return request.toBuilder() + .site(site != null ? modifySite(site, accountId) : null) + .app(site == null && app != null ? modifyApp(app, accountId) : app) + .imp(imps) + .build(); + } + + private static Site modifySite(Site site, String accountId) { + final Publisher originalPublisher = site.getPublisher(); + final Publisher newPublisher = originalPublisher != null + ? originalPublisher.toBuilder().id(accountId).build() + : Publisher.builder().id(accountId).build(); + return site.toBuilder().publisher(newPublisher).build(); + } + + private static App modifyApp(App app, String accountId) { + final Publisher originalPublisher = app.getPublisher(); + final Publisher newPublisher = originalPublisher != null + ? originalPublisher.toBuilder().id(accountId).build() + : Publisher.builder().id(accountId).build(); + return app.toBuilder().publisher(newPublisher).build(); + } + + private String resolveEndpointUrl(String subdomain) { + return endpointUrl.replace(SUBDOMAIN_MACRO, StringUtils.defaultIfBlank(subdomain, defaultSubdomain)); + } + + @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 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, getBidType(bid), bidResponse.getCur())) + .collect(Collectors.toList()); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> throw new PreBidException( + "could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java b/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java index b0b960caa7c..789ceb99681 100644 --- a/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java +++ b/src/main/java/org/prebid/server/bidder/revcontent/RevcontentBidder.java @@ -90,4 +90,3 @@ private static BidType resolveBidType(Bid bid) { : BidType.xNative; } } - diff --git a/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java b/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java index 05a5dafa212..d51423e83af 100644 --- a/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java @@ -92,14 +92,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, - List errors) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -110,12 +108,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) { @@ -132,6 +130,7 @@ private static BidType resolveBidType(Bid bid) throws PreBidException { return switch (markupType) { case 1 -> BidType.banner; case 2 -> BidType.video; + case 4 -> BidType.xNative; default -> throw new PreBidException("Unsupported MType: %s, for bid: %s".formatted(markupType, bid.getId())); }; 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..8d45320cb9c 100644 --- a/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java +++ b/src/main/java/org/prebid/server/bidder/rtbhouse/RtbhouseBidder.java @@ -3,11 +3,14 @@ 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.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 com.iab.openrtb.response.Bid; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; @@ -22,6 +25,7 @@ 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.rtbhouse.ExtImpRtbhouse; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.util.BidderUtil; @@ -34,6 +38,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class RtbhouseBidder implements Bidder { @@ -41,6 +46,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; @@ -59,6 +65,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List modifiedImps = new ArrayList<>(); final List errors = new ArrayList<>(); + String publisherId = null; for (Imp imp : bidRequest.getImp()) { try { @@ -66,23 +73,131 @@ public Result>> makeHttpRequests(BidRequest bidRequ final Price bidFloorPrice = resolveBidFloor(imp, impExt, bidRequest); modifiedImps.add(modifyImp(imp, bidFloorPrice)); + if (publisherId == null) { + publisherId = impExt.getPublisherId(); + } } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - if (errors.size() > 0) { + if (!errors.isEmpty()) { return Result.withErrors(errors); } final BidRequest outgoingRequest = bidRequest.toBuilder() .cur(Collections.singletonList(BIDDER_CURRENCY)) + .site(modifySite(bidRequest.getSite(), publisherId)) .imp(modifiedImps) .build(); return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); } + private ExtImpRtbhouse parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), RTBHOUSE_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private Price resolveBidFloor(Imp imp, ExtImpRtbhouse impExt, BidRequest bidRequest) { + final List brCur = bidRequest.getCur(); + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + + final BigDecimal impExtBidFloor = impExt.getBidFloor(); + 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; + + return BidderUtil.isValidPrice(resolvedPrice) + && !StringUtils.equalsIgnoreCase(resolvedPrice.getCurrency(), BIDDER_CURRENCY) + ? convertBidFloor(resolvedPrice, imp.getId(), bidRequest) + : resolvedPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { + final String bidFloorCur = bidFloorPrice.getCurrency(); + try { + final BigDecimal convertedPrice = currencyConversionService + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } catch (PreBidException e) { + throw new PreBidException(String.format( + "Unable to convert provided bid floor currency from %s to %s for imp `%s`", + bidFloorCur, BIDDER_CURRENCY, impId)); + } + } + + private static Imp modifyImp(Imp imp, Price bidFloorPrice) { + return imp.toBuilder() + .tagid(extractTagId(imp)) + .bidfloorcur(ObjectUtil.getIfNotNull(bidFloorPrice, Price::getCurrency)) + .bidfloor(ObjectUtil.getIfNotNull(bidFloorPrice, Price::getValue)) + .pmp(null) + .build(); + } + + private static String extractTagId(Imp imp) { + return Optional.ofNullable(imp.getTagid()) + .filter(StringUtils::isNotBlank) + .or(() -> extractGpid(imp)) + .or(() -> extractAdslot(imp)) + .or(() -> extractPbadslot(imp)) + .or(() -> Optional.ofNullable(imp.getId()) + .filter(StringUtils::isNotBlank)) + .orElse(null); + } + + private static Optional extractGpid(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("gpid")) + .map(JsonNode::textValue) + .filter(StringUtils::isNotBlank); + } + + private static Optional extractAdslot(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("data")) + .map(data -> data.get("adserver")) + .map(adserver -> adserver.get("adslot")) + .map(JsonNode::textValue) + .filter(StringUtils::isNotBlank); + } + + private static Optional extractPbadslot(Imp imp) { + return Optional.ofNullable(imp.getExt()) + .map(ext -> ext.get("data")) + .map(data -> data.get("pbadslot")) + .map(JsonNode::textValue) + .filter(StringUtils::isNotBlank); + } + + private Site modifySite(Site site, String publisherId) { + final ObjectNode prebidNode = mapper.mapper().createObjectNode(); + prebidNode.put("publisherId", publisherId); + + final ExtPublisher extPublisher = ExtPublisher.empty(); + extPublisher.addProperty("prebid", prebidNode); + + final Publisher publisher = Optional.ofNullable(site) + .map(Site::getPublisher) + .map(Publisher::toBuilder) + .orElseGet(Publisher::builder) + .ext(extPublisher) + .build(); + + return Optional.ofNullable(site) + .map(Site::toBuilder) + .orElseGet(Site::builder) + .publisher(publisher) + .build(); + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -127,29 +242,12 @@ private BidderBid resolveBidderBid(Bid bid, .build(); return BidderBid.builder() - .bid(updatedBid) + .bid(resolveMacros(updatedBid)) .type(bidType) .bidCurrency(currency) .build(); } - private String resolveNativeAdm(String adm, List bidderErrors) { - final JsonNode admNode; - try { - admNode = mapper.mapper().readTree(adm); - } catch (JsonProcessingException e) { - bidderErrors.add(BidderError.badServerResponse("Unable to parse native adm: %s".formatted(adm))); - return null; - } - - final JsonNode nativeNode = admNode.get("native"); - if (nativeNode != null) { - return nativeNode.toString(); - } - - return adm; - } - private static BidType getBidType(String impId, List imps) { for (Imp imp : imps) { if (imp.getId().equals(impId)) { @@ -157,57 +255,38 @@ 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; } } } return BidType.banner; } - private ExtImpRtbhouse parseImpExt(Imp imp) { + private String resolveNativeAdm(String adm, List bidderErrors) { + final JsonNode admNode; try { - return mapper.mapper().convertValue(imp.getExt(), RTBHOUSE_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); + admNode = mapper.mapper().readTree(adm); + } catch (JsonProcessingException e) { + bidderErrors.add(BidderError.badServerResponse("Unable to parse native adm: %s".formatted(adm))); + return null; } - } - - private static Imp modifyImp(Imp imp, Price bidFloorPrice) { - - return imp.toBuilder() - .bidfloorcur(ObjectUtil.getIfNotNull(bidFloorPrice, Price::getCurrency)) - .bidfloor(ObjectUtil.getIfNotNull(bidFloorPrice, Price::getValue)) - .build(); - } - - private Price resolveBidFloor(Imp imp, ExtImpRtbhouse impExt, BidRequest bidRequest) { - final List brCur = bidRequest.getCur(); - 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 Price impExtBidFloorPrice = Price.of(impExtCurrency, impExtBidFloor); - final Price resolvedPrice = initialBidFloorPrice.getValue() == null - ? impExtBidFloorPrice : initialBidFloorPrice; + final JsonNode nativeNode = admNode.get("native"); + if (nativeNode != null) { + return nativeNode.toString(); + } - return BidderUtil.isValidPrice(resolvedPrice) - && !StringUtils.equalsIgnoreCase(resolvedPrice.getCurrency(), BIDDER_CURRENCY) - ? convertBidFloor(resolvedPrice, imp.getId(), bidRequest) - : resolvedPrice; + return adm; } - private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { - final String bidFloorCur = bidFloorPrice.getCurrency(); - try { - final BigDecimal convertedPrice = currencyConversionService - .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, BIDDER_CURRENCY); + private static Bid resolveMacros(Bid bid) { + final BigDecimal price = bid.getPrice(); + final String priceAsString = price != null ? price.toPlainString() : "0"; - return Price.of(BIDDER_CURRENCY, convertedPrice); - } catch (PreBidException e) { - throw new PreBidException(String.format( - "Unable to convert provided bid floor currency from %s to %s for imp `%s`", - bidFloorCur, BIDDER_CURRENCY, impId)); - } + 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..5760b9f4242 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java @@ -1,6 +1,5 @@ package org.prebid.server.bidder.rubicon; -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; @@ -29,8 +28,6 @@ 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.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -53,7 +50,6 @@ import org.prebid.server.bidder.rubicon.proto.request.RubiconExtPrebidBiddersBidder; import org.prebid.server.bidder.rubicon.proto.request.RubiconExtPrebidBiddersBidderDebug; import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExt; -import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtPrebid; import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRp; import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRpRtb; import org.prebid.server.bidder.rubicon.proto.request.RubiconImpExtRpTrack; @@ -77,19 +73,20 @@ import org.prebid.server.floors.PriceFloorResolver; import org.prebid.server.floors.model.PriceFloorResult; import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.identity.IdGenerator; 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; import org.prebid.server.proto.openrtb.ext.request.ExtPublisher; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; @@ -109,6 +106,7 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.version.PrebidVersionProvider; import java.math.BigDecimal; import java.net.URISyntaxException; @@ -126,30 +124,23 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; public class RubiconBidder implements Bidder { private static final Logger logger = LoggerFactory.getLogger(RubiconBidder.class); - private static final ConditionalLogger MISSING_VIDEO_SIZE_LOGGER = + private static final ConditionalLogger missingVideoSizeLogger = new ConditionalLogger("missing_video_size", logger); 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"; @@ -157,16 +148,24 @@ public class RubiconBidder implements Bidder { private static final String FPD_KEYWORDS_FIELD = "keywords"; private static final String DFP_ADUNIT_CODE_FIELD = "dfp_ad_unit_code"; private static final String STYPE_FIELD = "stype"; + private static final String TID_FIELD = "tid"; 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); + private static final Set SITE_SEGTAXES = Set.of(1, 2, 5, 6, 7); private static final Set STYPE_TO_REMOVE = new HashSet<>(Arrays.asList(PPUID_STYPE, SHA256EMAIL_STYPE, DMP_STYPE)); @@ -178,29 +177,46 @@ 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 String apexRendererUrl; private final CurrencyConversionService currencyConversionService; private final PriceFloorResolver floorResolver; + private final PrebidVersionProvider versionProvider; + private final IdGenerator idGenerator; 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, + String apexRendererUrl, CurrencyConversionService currencyConversionService, PriceFloorResolver floorResolver, + PrebidVersionProvider versionProvider, + IdGenerator idGenerator, 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.apexRendererUrl = apexRendererUrl; this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.floorResolver = Objects.requireNonNull(floorResolver); + this.versionProvider = Objects.requireNonNull(versionProvider); + this.idGenerator = Objects.requireNonNull(idGenerator); this.mapper = Objects.requireNonNull(mapper); headers = headers(Objects.requireNonNull(xapiUsername), Objects.requireNonNull(xapiPassword)); @@ -225,9 +241,10 @@ public Result>> makeHttpRequests(BidRequest bidRequ try { final Imp imp = impToExt.getKey(); final ExtImpRubicon impExt = impToExt.getValue(); + final String pbBidId = generateBidId ? idGenerator.generateId() : null; final List impBidRequests = isMultiformatEnabled(impExt) - ? createMultiFormatRequests(bidRequest, imp, impExt, language, errors) - : List.of(createSingleRequest(bidRequest, imp, impExt, null, language, errors)); + ? createMultiFormatRequests(bidRequest, imp, impExt, pbBidId, language, errors) + : List.of(createSingleRequest(bidRequest, imp, impExt, pbBidId, null, language, errors)); httpRequests.addAll(createImpHttpRequests(imp, impBidRequests, uri)); } catch (PreBidException e) { @@ -241,6 +258,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ private List createMultiFormatRequests(BidRequest bidRequest, Imp imp, ExtImpRubicon impExt, + String pbBidId, String language, List errors) { @@ -248,13 +266,14 @@ private List createMultiFormatRequests(BidRequest bidRequest, final Set formats = impByType.keySet(); if (formats.size() == 1) { return Collections.singletonList( - createSingleRequest(bidRequest, imp, impExt, null, language, errors)); + createSingleRequest(bidRequest, imp, impExt, pbBidId, null, language, errors)); } final List bidRequests = new ArrayList<>(); for (Imp singleFormatImp : impByType.values()) { try { - bidRequests.add(createSingleRequest(bidRequest, singleFormatImp, impExt, formats, language, errors)); + bidRequests.add( + createSingleRequest(bidRequest, singleFormatImp, impExt, pbBidId, formats, language, errors)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } @@ -318,7 +337,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(); } @@ -410,17 +429,18 @@ private static String firstImpExtLanguage(Collection rubiconImpEx private BidRequest createSingleRequest(BidRequest bidRequest, Imp imp, ExtImpRubicon extImpRubicon, + String pbBidId, Set formats, String impLanguage, List errors) { return bidRequest.toBuilder() - .imp(Collections.singletonList(makeImp(imp, extImpRubicon, bidRequest, formats, errors))) + .imp(Collections.singletonList(makeImp(imp, extImpRubicon, bidRequest, pbBidId, formats, errors))) .user(downgradeUserConsent(makeUser(bidRequest.getUser(), extImpRubicon))) .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 @@ -467,6 +487,7 @@ private RubiconExtPrebidBiddersBidder extPrebidBiddersRubicon(ExtRequest extRequ private Imp makeImp(Imp imp, ExtImpRubicon extImpRubicon, BidRequest bidRequest, + String pbImpId, Set formats, List errors) { @@ -491,16 +512,7 @@ private Imp makeImp(Imp imp, final Imp.ImpBuilder builder = imp.toBuilder() .metric(makeMetrics(imp)) .ext(mapper.mapper().valueToTree( - makeImpExt( - imp, - bidRequest, - extImpRubicon, - resolvedFormats, - site, - app, - extRequest, - ipfCurrency, - priceFloorResult))); + makeImpExt(imp, extImpRubicon, resolvedFormats, site, app, extRequest, pbImpId))); final BigDecimal resolvedBidFloor = ipfFloor != null ? convertToXAPICurrency(ipfFloor, ipfCurrency, imp, bidRequest) @@ -543,6 +555,7 @@ private PriceFloorResult resolvePriceFloors(BidRequest bidRequest, imp, mediaType, null, + bidderName, warnings); } @@ -599,7 +612,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) { @@ -655,19 +668,12 @@ private BigDecimal convertBidFloorCurrency(BigDecimal bidFloor, } private RubiconImpExt makeImpExt(Imp imp, - BidRequest bidRequest, ExtImpRubicon rubiconImpExt, Set formats, Site site, App app, ExtRequest extRequest, - String ipfResolvedCurrency, - PriceFloorResult priceFloorResult) { - - final ExtImpContext context = extImpContext(imp); - final RubiconImpExtPrebid rubiconImpExtPrebid = priceFloorResult != null - ? makeRubiconExtPrebid(priceFloorResult, ipfResolvedCurrency, imp, bidRequest) - : null; + String pbBidId) { final RubiconImpExtRpRtb rubiconImpExtRpRtb = CollectionUtils.isNotEmpty(formats) ? RubiconImpExtRpRtb.of(formats) @@ -675,9 +681,10 @@ 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); + rubiconImpExtRpRtb, + pbBidId); return RubiconImpExt.builder() .rp(rubiconImpExtRp) @@ -685,56 +692,24 @@ private RubiconImpExt makeImpExt(Imp imp, .maxbids(getMaxBids(extRequest)) .gpid(getGpid(imp.getExt())) .skadn(getSkadn(imp.getExt())) - .prebid(rubiconImpExtPrebid) + .tid(getTid(imp.getExt())) .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); - - return result.size() > 0 ? result : null; - } + mergeFirstPartyDataFromImp(imp, rubiconImpExt, result); - private RubiconImpExtPrebid makeRubiconExtPrebid(PriceFloorResult priceFloorResult, - String currency, - Imp imp, - BidRequest bidRequest) { - final ObjectNode impExt = imp.getExt(); - final ExtImpPrebid extImpPrebid = extImpPrebid(impExt.get(PREBID_EXT)); - final ExtImpPrebidFloors floors = extImpPrebid != null ? extImpPrebid.getFloors() : null; + result.put(PBS_LOGIN, xapiUsername); + result.put(PBS_VERSION, versionProvider.getNameVersionRecord()); + result.put(PBS_URL, externalUrl); - return RubiconImpExtPrebid.of(ExtImpPrebidFloors.of( - priceFloorResult.getFloorRule(), - convertToXAPICurrency(priceFloorResult.getFloorRuleValue(), currency, imp, bidRequest), - convertToXAPICurrency(priceFloorResult.getFloorValue(), currency, imp, bidRequest), - floors != null ? floors.getFloorMin() : null, - floors != null ? floors.getFloorMinCur() : null)); - } - - private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) { - try { - return mapper.mapper().treeToValue(extImpPrebid, ExtImpPrebid.class); - } catch (JsonProcessingException e) { - throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e); - } + return result; } private void mergeFirstPartyDataFromSite(Site site, ObjectNode result) { @@ -744,16 +719,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 +729,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)) { @@ -873,10 +811,6 @@ private static String getTextValueFromNodeByPath(JsonNode node, String path) { return nodeByPath != null && nodeByPath.isTextual() ? nodeByPath.textValue() : null; } - private static String getTextValueFromNode(JsonNode node) { - return node != null && node.isTextual() ? node.textValue() : null; - } - private void populateFirstPartyDataAttributes(ObjectNode sourceNode, ObjectNode targetNode) { if (sourceNode == null || sourceNode.isNull()) { return; @@ -956,15 +890,14 @@ private List mapVendorsNamesToUrls(List metrics) { return vendorsUrls.isEmpty() ? null : vendorsUrls; } - private Integer getMaxBids(ExtRequest extRequest) { + private static Integer getMaxBids(ExtRequest extRequest) { final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; final List multibids = extRequestPrebid != null ? extRequestPrebid.getMultibid() : null; final ExtRequestPrebidMultiBid extRequestPrebidMultiBid = - CollectionUtils.isNotEmpty(multibids) ? multibids.get(0) : null; - final Integer multibidMaxBids = extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; + CollectionUtils.isNotEmpty(multibids) ? multibids.getFirst() : null; - return multibidMaxBids != null ? multibidMaxBids : 1; + return extRequestPrebidMultiBid != null ? extRequestPrebidMultiBid.getMaxBids() : null; } private String getGpid(ObjectNode impExt) { @@ -977,19 +910,15 @@ 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; - final ObjectNode dataNode = toObjectNode(imp.getExt().get(FPD_DATA_FIELD)); - - return getAdSlot(contextDataNode, dataNode); + private String getTid(ObjectNode impExt) { + final JsonNode tidNode = impExt.get(TID_FIELD); + return tidNode != null && tidNode.isTextual() ? tidNode.asText() : null; } - 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)); + private String getAdSlot(Imp imp) { + final ObjectNode dataNode = toObjectNode(imp.getExt().get(FPD_DATA_FIELD)); + + return getAdSlotFromAdServer(dataNode); } private String getAdSlotFromAdServer(JsonNode dataNode) { @@ -1042,12 +971,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,37 +983,33 @@ 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) { - MISSING_VIDEO_SIZE_LOGGER.warn( + missingVideoSizeLogger.warn( "RP adapter: video request with no size_id. Referrer URL = %s, impId = %s" .formatted(referer, impId), 0.01d); } } - 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 +1084,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 +1097,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 +1116,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 +1198,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 +1210,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 +1227,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 +1251,40 @@ private void mergeFirstPartyDataFromUser(ExtUser userExt, ObjectNode result) { } } - private static void enrichWithIabAttribute(ObjectNode target, List data, Set segtaxValues) { - final List iabValue = CollectionUtils.emptyIfNull(data).stream() + private static void enrichWithIabAndSegtaxAttribute(ObjectNode target, List data, Set iabTaxes) { + CollectionUtils.emptyIfNull(data).stream() .filter(Objects::nonNull) - .filter(dataRecord -> containsSegtaxValue(dataRecord.getExt(), segtaxValues)) - .map(Data::getSegment) - .filter(Objects::nonNull) - .flatMap(segments -> segments.stream() - .map(Segment::getId)) - .filter(Objects::nonNull) - .toList(); + .flatMap(dataEntry -> extractTaxToSegmentId(dataEntry, iabTaxes)) + .limit(MAX_NUMBER_OF_SEGMENTS) + .forEach(entry -> getArrayNodeOrCreate(target, entry.getKey()).add(entry.getValue())); + } - if (CollectionUtils.isNotEmpty(iabValue)) { - final ArrayNode iab = target.putArray("iab"); - iabValue.forEach(iab::add); + private static Stream> extractTaxToSegmentId(Data data, Set iabTaxes) { + final ObjectNode ext = data.getExt(); + final JsonNode taxonomyId = ext != null ? ext.get(SEGTAX) : null; + if (taxonomyId == null || !taxonomyId.isInt()) { + return Stream.empty(); } + + final String taxKey = resolveTaxName(taxonomyId.intValue(), iabTaxes); + return CollectionUtils.emptyIfNull(data.getSegment()).stream() + .filter(Objects::nonNull) + .map(Segment::getId) + .filter(StringUtils::isNotBlank) + .map(id -> Map.entry(taxKey, id)); + } + + private static String resolveTaxName(Integer taxonomyId, Set iabTaxes) { + return iabTaxes.contains(taxonomyId) ? SEGTAX_IAB : SEGTAX_TAX + taxonomyId; } - private static boolean containsSegtaxValue(ObjectNode ext, Set segtaxValues) { - final JsonNode taxonomyName = ext != null ? ext.get("segtax") : null; + private static ArrayNode getArrayNodeOrCreate(ObjectNode parent, String field) { + final JsonNode node = parent.get(field); + if (node == null || !node.isArray()) { + return parent.putArray(field); + } - return taxonomyName != null && taxonomyName.isInt() && segtaxValues.contains(taxonomyName.intValue()); + return (ArrayNode) node; } private void processWarnings(List errors, List priceFloorsWarnings) { @@ -1445,7 +1356,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 +1387,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 +1449,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 +1464,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() @@ -1579,8 +1485,12 @@ private ObjectNode modifyRubiconImpExt(ObjectNode impExtNode, ExtRequest extRequ ? mapper.mapper().createObjectNode() : (ObjectNode) impExtRp.getTarget(); final ObjectNode modifiedTargetNode = targetNode.put("line_item", extLineItemId); - final RubiconImpExtRp modifiedImpExtRp = RubiconImpExtRp.of(impExtRp.getZoneId(), modifiedTargetNode, - impExtRp.getTrack(), impExtRp.getRtb()); + final RubiconImpExtRp modifiedImpExtRp = RubiconImpExtRp.of( + impExtRp.getZoneId(), + modifiedTargetNode, + impExtRp.getTrack(), + impExtRp.getRtb(), + impExtRp.getPbBidId()); return mapper.mapper().valueToTree(rubiconImpExt.toBuilder() .rp(modifiedImpExtRp) @@ -1603,51 +1513,103 @@ private List bidsFromResponse(BidRequest prebidRequest, BidRequest bidRequest, RubiconBidResponse bidResponse, List errors) { + final Map idToImp = prebidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); final Map idToRubiconImp = bidRequest.getImp().stream() .collect(Collectors.toMap(Imp::getId, Function.identity())); - final Float cpmOverrideFromRequest = cpmOverrideFromRequest(prebidRequest); + final RubiconExtPrebidBiddersBidder extPrebidBiddersBidder = extPrebidBiddersRubicon(prebidRequest.getExt()); + final Float cpmOverrideFromRequest = cpmOverrideFromRequest(extPrebidBiddersBidder); + final boolean hasApexRenderer = hasApexRenderer(extPrebidBiddersBidder); final BidType bidType = bidType(bidRequest); return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) - .map(seatBid -> updateSeatBids(seatBid, errors)) - .map(RubiconSeatBid::getBid) - .filter(Objects::nonNull) + .map(seatBid -> seatBid.getBid().stream() + .filter(Objects::nonNull) + .map(bid -> updateBid( + bid, + seatBid, + idToImp.get(bid.getImpid()), + idToRubiconImp.get(bid.getImpid()), + bidType, + cpmOverrideFromRequest, + hasApexRenderer, + errors)) + .filter(Objects::nonNull) + .map(bid -> createBidderBid( + bid, + idToRubiconImp.get(bid.getImpid()), + bidType, + bidResponse.getCur())) + .toList()) .flatMap(Collection::stream) - .map(bid -> updateBid(bid, idToImp.get(bid.getImpid()), cpmOverrideFromRequest, bidResponse)) - .map(bid -> createBidderBid(bid, idToRubiconImp.get(bid.getImpid()), bidType, bidResponse.getCur())) .toList(); } - private RubiconSeatBid updateSeatBids(RubiconSeatBid seatBid, List errors) { - final String buyer = seatBid.getBuyer(); - final int networkId = NumberUtils.toInt(buyer, 0); - if (networkId <= 0) { - return seatBid; - } - final List updatedBids = seatBid.getBid().stream() - .map(bid -> insertNetworkIdToMeta(bid, networkId, errors)) - .filter(Objects::nonNull) - .toList(); - return seatBid.toBuilder().bid(updatedBids).build(); - } + private Bid updateBid(RubiconBid bid, + RubiconSeatBid seatBid, + Imp imp, + Imp rubiconImp, + BidType bidType, + Float cpmOverrideFromRequest, + boolean hasApexRenderer, + List errors) { - private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List errors) { - final ObjectNode bidExt = bid.getExt(); - final ExtPrebid extPrebid; + final ObjectNode updateBidExt; try { - extPrebid = getExtPrebid(bidExt, bid.getId()); + updateBidExt = prepareBidExt(bid, seatBid, imp, bidType, hasApexRenderer); } catch (PreBidException e) { errors.add(BidderError.badServerResponse(e.getMessage())); return null; } + + // Unconditionally set price if coming from CPM override + final Float cpmOverride = ObjectUtils.defaultIfNull(cpmOverrideFromImp(imp), cpmOverrideFromRequest); + final BigDecimal bidPrice = cpmOverride != null + ? new BigDecimal(String.valueOf(cpmOverride)) + : bid.getPrice(); + + final RubiconBid updatedRubiconBid = bid.toBuilder() + .id(resolveBidId(rubiconImp, bid)) + .adm(resolveAdm(bid.getAdm(), bid.getAdmNative())) + .price(bidPrice) + .ext(updateBidExt) + .build(); + + return bidFromRubiconBid(updatedRubiconBid); + } + + private ObjectNode prepareBidExt(RubiconBid bid, + RubiconSeatBid seatBid, + Imp imp, + BidType bidType, + boolean hasApexRenderer) { + + final ObjectNode bidExt = bid.getExt(); + final ExtPrebid extPrebid = getExtPrebid(bidExt, bid.getId()); final ExtBidPrebid extBidPrebid = extPrebid != null ? extPrebid.getPrebid() : null; final ExtBidPrebidMeta meta = extBidPrebid != null ? extBidPrebid.getMeta() : null; - final ExtBidPrebidMeta updatedMeta = meta != null - ? meta.toBuilder().networkId(networkId).build() - : ExtBidPrebidMeta.builder().networkId(networkId).build(); + + final Integer networkId = resolveNetworkId(seatBid); + final String seat = seatBid.getSeat(); + final String rendererUrl = resolveRendererUrl(imp, meta, bidType, hasApexRenderer); + final List advertiserDomains = bid.getAdomain(); + final Integer advertiserId = resolveAdvertiserId(bidExt); + + if (ObjectUtils.allNull(networkId, rendererUrl, seat, advertiserDomains, advertiserId)) { + return bidExt; + } + + final ExtBidPrebidMeta updatedMeta = Optional.ofNullable(meta) + .map(ExtBidPrebidMeta::toBuilder) + .orElseGet(ExtBidPrebidMeta::builder) + .networkId(networkId) + .seat(seat) + .rendererUrl(rendererUrl) + .advertiserId(advertiserId) + .advertiserDomains(advertiserDomains) + .build(); final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid != null ? extBidPrebid.toBuilder().meta(updatedMeta).build() @@ -1656,7 +1618,7 @@ private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List getExtPrebid(ObjectNode bidExt, String bidId) { @@ -1667,31 +1629,66 @@ private ExtPrebid getExtPrebid(ObjectNode bidExt, Stri } } - private Bid updateBid(RubiconBid bid, Imp imp, Float cpmOverrideFromRequest, RubiconBidResponse bidResponse) { - String bidId = bid.getId(); - if (generateBidId) { - // Since Rubicon XAPI returns openrtb_response.seatbid.bid.id not unique enough - // generate new value for it - bidId = UUID.randomUUID().toString(); - } else if (Objects.equals(bid.getId(), "0")) { - // Since Rubicon XAPI returns only one bid per response - // copy bidResponse.bidid to openrtb_response.seatbid.bid.id - bidId = bidResponse.getBidid(); + private static Integer resolveNetworkId(RubiconSeatBid seatBid) { + final String buyer = seatBid.getBuyer(); + final int networkId = NumberUtils.toInt(buyer, 0); + return networkId <= 0 ? null : networkId; + } + + private String resolveRendererUrl(Imp imp, ExtBidPrebidMeta meta, BidType bidType, boolean hasApexRenderer) { + if (imp == null) { + return null; } - // Unconditionally set price if coming from CPM override - final Float cpmOverride = ObjectUtils.defaultIfNull(cpmOverrideFromImp(imp), cpmOverrideFromRequest); - final BigDecimal bidPrice = cpmOverride != null - ? new BigDecimal(String.valueOf(cpmOverride)) - : bid.getPrice(); + final Video video = imp.getVideo(); + return hasApexRenderer + && (bidType == BidType.video || isVideoMetaMediaType(meta)) + && (video != null && !Objects.equals(video.getPlacement(), 1) && !Objects.equals(video.getPlcmt(), 1)) + ? apexRendererUrl + : null; + } - final RubiconBid updatedRubiconBid = bid.toBuilder() - .id(bidId) - .adm(resolveAdm(bid.getAdm(), bid.getAdmNative())) - .price(bidPrice) - .build(); + private static Boolean isVideoMetaMediaType(ExtBidPrebidMeta meta) { + return Optional.ofNullable(meta) + .map(ExtBidPrebidMeta::getMediaType) + .map("video"::equalsIgnoreCase) + .orElse(false); + } - return bidFromRubiconBid(updatedRubiconBid); + private static Integer resolveAdvertiserId(ObjectNode bidExt) { + return Optional.ofNullable(bidExt) + .map(ext -> ext.get("rp")) + .filter(JsonNode::isObject) + .map(rp -> rp.get("advid")) + .map(RubiconBidder::convertToInt) + .orElse(null); + } + + private static Integer convertToInt(JsonNode jsonNode) { + if (jsonNode.canConvertToInt()) { + return jsonNode.asInt(); + } + + if (jsonNode.isTextual()) { + try { + return Integer.parseInt(jsonNode.asText()); + } catch (NumberFormatException e) { + return null; + } + } + + return null; + } + + private String resolveBidId(Imp rubiconImp, RubiconBid bid) { + return generateBidId + ? Optional.ofNullable(rubiconImp) + .map(Imp::getExt) + .map(ext -> ext.get("rp")) + .map(rp -> rp.get("pb_bid_id")) + .map(JsonNode::asText) + .orElse(bid.getId()) + : bid.getId(); } private String resolveAdm(String bidAdm, ObjectNode admobject) { @@ -1711,7 +1708,6 @@ private Bid bidFromRubiconBid(RubiconBid rubiconBid) { } private static BidderBid createBidderBid(Bid bid, Imp imp, BidType bidType, String currency) { - return BidderBid.builder() .bid(bid) .type(bidType) @@ -1720,8 +1716,7 @@ private static BidderBid createBidderBid(Bid bid, Imp imp, BidType bidType, Stri .build(); } - private Float cpmOverrideFromRequest(BidRequest bidRequest) { - final RubiconExtPrebidBiddersBidder bidder = extPrebidBiddersRubicon(bidRequest.getExt()); + private static Float cpmOverrideFromRequest(RubiconExtPrebidBiddersBidder bidder) { final RubiconExtPrebidBiddersBidderDebug debug = bidder != null ? bidder.getDebug() : null; return debug != null ? debug.getCpmoverride() : null; } @@ -1734,8 +1729,12 @@ private Float cpmOverrideFromImp(Imp imp) { .orElse(null); } + private static boolean hasApexRenderer(RubiconExtPrebidBiddersBidder bidder) { + return Optional.ofNullable(bidder).map(RubiconExtPrebidBiddersBidder::getApexRenderer).orElse(false); + } + 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/rubicon/proto/request/RubiconAppExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java index 64460e5a725..905527302b9 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconAppExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconAppExt { RubiconSiteExtRp rp; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java index 146d302d7b1..95625d80fd7 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconBannerExt { RubiconBannerExtRp rp; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java index 0acbe640328..6aaadbb26f0 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconBannerExtRp.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconBannerExtRp { String mime; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java index bfc6c1c6f2b..2d3ea5de017 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconDeviceExt { RubiconDeviceExtRp rp; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java index 35ddea95331..6cfcb37b934 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconDeviceExtRp.java @@ -1,12 +1,10 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconDeviceExtRp { BigDecimal pixelratio; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java index 786ca0f2a1d..d7fa100c490 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBidders.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconExtPrebidBidders { RubiconExtPrebidBiddersBidder bidder; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java index 059c7ac7439..f2988ffe165 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconExtPrebidBiddersBidder.java @@ -1,13 +1,15 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconExtPrebidBiddersBidder { String integration; RubiconExtPrebidBiddersBidderDebug debug; + + @JsonProperty("apexRenderer") + Boolean apexRenderer; } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java index 1c39f9d9345..d40333b891a 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java @@ -20,5 +20,7 @@ public class RubiconImpExt { ObjectNode skadn; + String tid; + RubiconImpExtPrebid prebid; } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java index 103ee937c1c..fae19232ebe 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRp.java @@ -13,4 +13,6 @@ public class RubiconImpExtRp { RubiconImpExtRpTrack track; RubiconImpExtRpRtb rtb; + + String pbBidId; } diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java index 8902cf6aefd..0ff71bc5aaa 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExtRpTrack.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconImpExtRpTrack { String mint; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java index af3026a696d..507a0354b8c 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconPubExt { RubiconPubExtRp rp; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java index d1b92192638..274ff2c4576 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconPubExtRp.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconPubExtRp { Integer accountId; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java index 7cda21ef7ef..7c8c7f88d33 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconSiteExtRp.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.rubicon.proto.request; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconSiteExtRp { Integer siteId; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java index ece72cb4198..ee8ff245729 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargeting.java @@ -1,12 +1,10 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconTargeting { String key; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java index f1f1878d721..23fcfc62e86 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconTargetingExt { RubiconTargetingExtRp rp; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java index 65bcf0d58e0..75835a959c3 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconTargetingExtRp.java @@ -1,12 +1,10 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconTargetingExtRp { List targeting; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java index 1d545488b4e..fe85f28ad09 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconVideoExt { Integer skip; diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java index 88b81e54932..602dd9f5409 100644 --- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java +++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconVideoExtRp.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.rubicon.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class RubiconVideoExtRp { Integer sizeId; 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/seedtag/SeedtagBidder.java b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java new file mode 100644 index 00000000000..ea15def357c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java @@ -0,0 +1,153 @@ +package org.prebid.server.bidder.seedtag; + +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.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.seedtag.ExtImpSeedtag; +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 SeedtagBidder implements Bidder { + + private static final TypeReference> SEEDTAG_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BIDDER_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + private final CurrencyConversionService currencyConversionService; + + public SeedtagBidder(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 request) { + final List modifiedImps = new ArrayList<>(); + final List errors = new ArrayList<>(); + + for (Imp imp : request.getImp()) { + try { + final Price bidFloorPrice = resolveBidFloor(imp, request); + + modifiedImps.add(modifyImp(imp, bidFloorPrice)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.size() < 1) { + return Result.withErrors(errors); + } + + final BidRequest modifiedBidRequest = request.toBuilder() + .imp(modifiedImps) + .cur(Collections.singletonList(BIDDER_CURRENCY)) + .build(); + return Result.of( + Collections.singletonList(BidderUtil.defaultRequest(modifiedBidRequest, endpointUrl, mapper)), + errors); + } + + private static Imp modifyImp(Imp imp, Price bidFloorPrice) { + return imp.toBuilder() + .bidfloorcur(bidFloorPrice.getCurrency()) + .bidfloor(bidFloorPrice.getValue()) + .build(); + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, BIDDER_CURRENCY) + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { + final BigDecimal convertedPrice = currencyConversionService.convertCurrency( + bidFloorPrice.getValue(), + bidRequest, + bidFloorPrice.getCurrency(), + BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } + + @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<>(); + final List bidderBids = extractBids(bidResponse, errors); + return Result.of(bidderBids, 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 -> makeBidderBid(bid, errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBidderBid(Bid bid, List errors) { + final BidType bidType; + try { + bidType = getBidType(bid); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + + return BidderBid.of(bid, bidType, BIDDER_CURRENCY); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> throw new PreBidException("Invalid bid.mtype for bid.id: '%s'".formatted(bid.getId())); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/showheroes/ShowheroesBidder.java b/src/main/java/org/prebid/server/bidder/showheroes/ShowheroesBidder.java new file mode 100644 index 00000000000..b212950f322 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/showheroes/ShowheroesBidder.java @@ -0,0 +1,218 @@ +package org.prebid.server.bidder.showheroes; + +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.Imp; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +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.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.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; +import org.prebid.server.proto.openrtb.ext.request.showheroes.ExtImpShowheroes; +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.version.PrebidVersionProvider; + +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 ShowheroesBidder implements Bidder { + + private static final String BID_CURRENCY = "EUR"; + private static final String PBSP_JAVA = "java"; + private static final TypeReference> SHOWHEROES_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; + + private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; + private final JacksonMapper mapper; + private final String pbsVersion; + + public ShowheroesBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); + this.mapper = Objects.requireNonNull(mapper); + + this.pbsVersion = prebidVersionProvider.getNameVersionRecord(); + } + + @Override + public Result>> makeHttpRequests(BidRequest request) { + final BidderError validationError = validate(request.getSite(), request.getApp()); + if (validationError != null) { + return Result.withError(validationError); + } + + final List errors = new ArrayList<>(); + + final ExtRequestPrebidChannel prebidChannel = getPrebidChannel(request); + final List modifiedImps = new ArrayList<>(request.getImp().size()); + + for (Imp impression : request.getImp()) { + try { + modifiedImps.add(modifyImp(request, impression, prebidChannel)); + } catch (Exception e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + if (modifiedImps.isEmpty()) { + return Result.withErrors(errors); + } + + final Source source = modifySource(request); + final BidRequest modifiedRequest = request.toBuilder().imp(modifiedImps).source(source).build(); + final HttpRequest httpRequest = BidderUtil.defaultRequest(modifiedRequest, endpointUrl, mapper); + + return Result.of(Collections.singletonList(httpRequest), errors); + } + + private static BidderError validate(Site site, App app) { + if (site == null && app == null) { + return BidderError.badInput("BidRequest must contain one of site or app"); + } + if (site != null && site.getPage() == null) { + return BidderError.badInput("BidRequest.site.page is required"); + } + if (app != null && app.getBundle() == null) { + return BidderError.badInput("BidRequest.app.bundle is required"); + } + return null; + } + + private static ExtRequestPrebidChannel getPrebidChannel(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getChannel) + .orElse(null); + } + + private Imp modifyImp(BidRequest bidRequest, Imp imp, ExtRequestPrebidChannel prebidChannel) { + final ExtImpShowheroes extImpShowheroes = parseImpExt(imp); + + final boolean shouldSetDisplayManager = prebidChannel != null && imp.getDisplaymanager() == null; + final boolean shouldConvertFloor = shouldConvertFloor(imp); + + return imp.toBuilder() + .displaymanager(shouldSetDisplayManager ? prebidChannel.getName() : imp.getDisplaymanager()) + .displaymanagerver(shouldSetDisplayManager ? prebidChannel.getVersion() : imp.getDisplaymanagerver()) + .bidfloorcur(shouldConvertFloor ? BID_CURRENCY : imp.getBidfloorcur()) + .bidfloor(shouldConvertFloor ? resolveBidFloor(bidRequest, imp) : imp.getBidfloor()) + .ext(modifyImpExt(imp.getExt(), extImpShowheroes)) + .build(); + } + + private ExtImpShowheroes parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), SHOWHEROES_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException(e.getMessage()); + } + } + + private ObjectNode modifyImpExt(ObjectNode impExt, ExtImpShowheroes shImpExt) { + impExt.set("params", mapper.mapper().createObjectNode().put("unitId", shImpExt.getUnitId())); + return impExt; + } + + private static boolean shouldConvertFloor(Imp imp) { + return BidderUtil.isValidPrice(imp.getBidfloor()) + && !StringUtils.equalsIgnoreCase(imp.getBidfloorcur(), BID_CURRENCY); + } + + private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) { + return currencyConversionService.convertCurrency( + imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), BID_CURRENCY); + } + + private Source modifySource(BidRequest bidRequest) { + if (pbsVersion == null) { + return bidRequest.getSource(); + } + + final Source source = bidRequest.getSource(); + + final ExtSource extSource = Optional.ofNullable(source) + .map(Source::getExt) + .orElse(ExtSource.of(null)); + final ObjectNode prebidExtSource = Optional.ofNullable(extSource.getProperty("pbs")) + .filter(JsonNode::isObject) + .map(ObjectNode.class::cast) + .orElseGet(mapper.mapper()::createObjectNode) + .put("pbsv", pbsVersion) + .put("pbsp", PBSP_JAVA); + extSource.addProperty("pbs", prebidExtSource); + + return Optional.ofNullable(source) + .map(Source::toBuilder) + .orElseGet(Source::builder) + .ext(extSource) + .build(); + } + + @Override + public 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 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())) + .filter(Objects::nonNull) + .toList(); + } + + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case null, default -> BidType.video; + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java index 5bbe37d2b77..2ab8323d075 100644 --- a/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java +++ b/src/main/java/org/prebid/server/bidder/silvermob/SilvermobBidder.java @@ -4,6 +4,7 @@ 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; @@ -98,7 +99,7 @@ private ExtImpSilvermob parseImpExt(Imp imp) { } private static Boolean isInvalidHost(String host) { - return !StringUtils.equalsAny(host, "eu", "us", "apac"); + return !StringUtils.equalsAny(host, "eu", "us", "apac", "global"); } private String resolveEndpoint(ExtImpSilvermob extImp) { @@ -142,30 +143,31 @@ private List extractBids(BidderCall httpCall) { if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { throw new PreBidException("Empty SeatBid array"); } - return bidsFromResponse(bidResponse, httpCall.getRequest().getPayload()); + return bidsFromResponse(bidResponse); } - private static List bidsFromResponse(BidResponse bidResponse, BidRequest bidRequest) { + 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.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; - } - } + 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.banner; + + 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/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..874c116c0d3 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,13 +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.2"; 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 IMP_EXT_SKADN_FIELD = "skadn"; + private static final String SMT_ADTYPE_NATIVE = "Native"; private static final int DEFAULT_TTL = 300; @@ -148,11 +143,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,14 +196,14 @@ 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"); final String adBreakId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdbreakId, "adbreakId"); return modifyBidRequest(bidRequest, publisherId, () -> - modifyImpsForAdBreak(imps, adBreakId, resolveImpExtSkadn(impExt))); + modifyImpsForAdBreak(imps, adBreakId, removeBidderNodeFromImpExt(impExt))); } catch (PreBidException | IllegalArgumentException e) { errors.add(BidderError.badInput(e.getMessage())); return null; @@ -216,27 +214,30 @@ 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(); } - private List modifyImpsForAdBreak(List imps, String adBreakId, ObjectNode impExtSkadn) { + private List modifyImpsForAdBreak(List imps, String adBreakId, ObjectNode impExt) { return IntStream.range(0, imps.size()) .mapToObj(idx -> - modifyImpForAdBreak(imps.get(idx), idx + 1, adBreakId, idx == 0 ? impExtSkadn : null)) + modifyImpForAdBreak(imps.get(idx), idx + 1, adBreakId, idx == 0 ? impExt : null)) .toList(); } - private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, ObjectNode impExtSkadn) { + private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, ObjectNode impExt) { final Video modifiedVideo = imp.getVideo().toBuilder() .sequence(sequence) .ext(mapper.mapper().createObjectNode().set("context", TextNode.valueOf("adpod"))) @@ -244,7 +245,7 @@ private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, Obj return imp.toBuilder() .tagid(adBreakId) .video(modifiedVideo) - .ext(impExtSkadn) + .ext(impExt) .build(); } @@ -291,45 +292,33 @@ private BidRequest prepareIndividualRequest(BidRequest bidRequest, Imp imp, List final String adSpaceId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdspaceId, "adspaceId"); return modifyBidRequest(bidRequest, publisherId, () -> - modifyImpForAdSpace(imp, adSpaceId, resolveImpExtSkadn(impExt))); + modifyImpForAdSpace(imp, adSpaceId, removeBidderNodeFromImpExt(impExt))); } catch (PreBidException | IllegalArgumentException e) { errors.add(BidderError.badInput(e.getMessage())); return null; } } - private ObjectNode resolveImpExtSkadn(ObjectNode impExt) { - if (!impExt.has(IMP_EXT_SKADN_FIELD)) { + private ObjectNode removeBidderNodeFromImpExt(ObjectNode impExt) { + if (impExt == null) { return null; - } else if (impExt.get(IMP_EXT_SKADN_FIELD).isEmpty() || !impExt.get(IMP_EXT_SKADN_FIELD).isObject()) { - throw new PreBidException("Invalid imp.ext.skadn"); - } else { - return mapper.mapper().createObjectNode().set(IMP_EXT_SKADN_FIELD, impExt.get(IMP_EXT_SKADN_FIELD)); } + + final ObjectNode impExtCopy = impExt.deepCopy(); + + impExtCopy.remove("bidder"); + return impExtCopy.isEmpty() ? null : impExtCopy; } - private List modifyImpForAdSpace(Imp imp, String adSpaceId, ObjectNode impExtSkadn) { + private List modifyImpForAdSpace(Imp imp, String adSpaceId, ObjectNode impExt) { final Imp modifiedImp = imp.toBuilder() .tagid(adSpaceId) - .banner(getIfNotNull(imp.getBanner(), SmaatoBidder::modifyBanner)) - .ext(impExtSkadn) + .ext(impExt) .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"); + private String extractAdmBanner(String adm, List curls) { + if (CollectionUtils.isEmpty(curls)) { + return adm; } 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"); - } - - 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))))); + curls.forEach(url -> clickEvent.append( + "fetch(decodeURIComponent('%s'.replace(/\\+/g, ' ')), {cache: 'no-cache'});" + .formatted(HttpUtil.encodeUrl(StringUtils.stripToEmpty(url))))); - final StringBuilder impressionTracker = new StringBuilder(); - CollectionUtils.emptyIfNull(richmedia.getImpressionTrackers()) - .forEach(tracker -> impressionTracker.append( - "\"\"".formatted(tracker))); - - 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/smartadserver/SmartadserverBidder.java b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java index 2622aae49cf..150c37fd255 100644 --- a/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartadserver/SmartadserverBidder.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.smartadserver; 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.request.Publisher; @@ -30,6 +31,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Objects; @@ -40,31 +42,51 @@ public class SmartadserverBidder implements Bidder { }; private final String endpointUrl; + private final String secondaryEndpointUrl; private final JacksonMapper mapper; - public SmartadserverBidder(String endpointUrl, JacksonMapper mapper) { + public SmartadserverBidder(String endpointUrl, String secondaryEndpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.secondaryEndpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(secondaryEndpointUrl)); this.mapper = Objects.requireNonNull(mapper); } @Override public Result>> makeHttpRequests(BidRequest request) { - final List> result = new ArrayList<>(); final List errors = new ArrayList<>(); + final List modifiedImps = new ArrayList<>(); + final LinkedHashMap impToExtImpMap = new LinkedHashMap<>(); + + boolean isProgrammaticGuaranteed = false; for (Imp imp : request.getImp()) { try { - final ExtImpSmartadserver extImpSmartadserver = parseImpExt(imp); - final BidRequest updatedRequest = request.toBuilder() - .imp(Collections.singletonList(imp)) - .site(modifySite(request.getSite(), extImpSmartadserver.getNetworkId())) - .build(); - result.add(createSingleRequest(updatedRequest)); + final ExtImpSmartadserver extImp = parseImpExt(imp); + isProgrammaticGuaranteed |= extImp.isProgrammaticGuaranteed(); + impToExtImpMap.put(imp, extImp); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); } } - return Result.of(result, errors); + + if (impToExtImpMap.isEmpty()) { + return Result.withErrors(errors); + } + + final String extImpKey = isProgrammaticGuaranteed ? "smartadserver" : "bidder"; + impToExtImpMap.forEach((imp, extImp) -> modifiedImps.add(modifyImp(imp, extImp, extImpKey))); + + final ExtImpSmartadserver lastExtImp = impToExtImpMap.lastEntry().getValue(); + final BidRequest outgoingRequest = request.toBuilder() + .imp(modifiedImps) + .site(modifySite(request.getSite(), lastExtImp.getNetworkId())) + .build(); + + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + makeUrl(isProgrammaticGuaranteed), + mapper); + return Result.of(Collections.singletonList(httpRequest), errors); } private ExtImpSmartadserver parseImpExt(Imp imp) { @@ -75,22 +97,11 @@ private ExtImpSmartadserver parseImpExt(Imp imp) { } } - private HttpRequest createSingleRequest(BidRequest request) { - - return BidderUtil.defaultRequest(request, getUri(), mapper); - } - - private String getUri() { - final URI uri; - try { - uri = new URI(endpointUrl); - } catch (URISyntaxException e) { - throw new PreBidException("Malformed URL: %s.".formatted(endpointUrl)); - } - return new URIBuilder(uri) - .setPath(StringUtils.removeEnd(uri.getPath(), "/") + "/api/bid") - .addParameter("callerId", "5") - .toString(); + private Imp modifyImp(Imp imp, ExtImpSmartadserver extImp, String impExtKey) { + final ObjectNode impExt = imp.getExt().deepCopy(); + impExt.remove("bidder"); + impExt.set(impExtKey, mapper.mapper().valueToTree(extImp)); + return imp.toBuilder().ext(impExt).build(); } private static Site modifySite(Site site, Integer networkId) { @@ -108,17 +119,35 @@ private static Publisher modifyPublisher(Publisher publisher, Integer networkId) return publisherBuilder.id(String.valueOf(networkId)).build(); } + private String makeUrl(boolean isProgrammaticGuaranteed) { + final String url = isProgrammaticGuaranteed ? secondaryEndpointUrl : endpointUrl; + try { + final URI uri = new URI(url); + final String path = isProgrammaticGuaranteed ? "/ortb" : "/api/bid"; + final URIBuilder uriBuilder = new URIBuilder(uri) + .setPath(StringUtils.removeEnd(uri.getPath(), "/") + path); + + if (!isProgrammaticGuaranteed) { + uriBuilder.addParameter("callerId", "5"); + } + + return uriBuilder.toString(); + } catch (URISyntaxException e) { + throw new PreBidException("Malformed URL: %s.".formatted(url)); + } + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); - return extractBids(httpCall.getRequest().getPayload(), bidResponse); + return extractBids(bidResponse); } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private Result> extractBids(BidRequest bidRequest, BidResponse bidResponse) { + private Result> extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Result.empty(); } @@ -128,19 +157,17 @@ private Result> extractBids(BidRequest bidRequest, BidResponse b .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, getBidTypeFromMarkupType(bid.getMtype()), bidResponse.getCur())) .toList(); return Result.of(bidderBids, errors); } - private static BidType getBidType(String impId, List imps) { - for (Imp imp : imps) { - if (imp.getId().equals(impId)) { - return imp.getVideo() != null - ? BidType.video - : (imp.getXNative() != null ? BidType.xNative : BidType.banner); - } - } - return BidType.banner; + private static BidType getBidTypeFromMarkupType(Integer markupType) { + return switch (markupType) { + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> BidType.banner; + }; } } 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..73df9290df2 100644 --- a/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java +++ b/src/main/java/org/prebid/server/bidder/smarthub/SmarthubBidder.java @@ -11,6 +11,7 @@ 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; @@ -45,7 +46,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(); @@ -67,7 +68,7 @@ private MultiMap resolveHeaders() { } private String buildEndpointUrl(ExtImpSmarthub extImpSmarthub) { - return endpointTemplate.replace("{{Host}}", extImpSmarthub.getPartnerName()) + return endpointTemplate.replace("{{Host}}", StringUtils.defaultString(extImpSmarthub.getPartnerName())) .replace("{{AccountID}}", extImpSmarthub.getSeat()) .replace("{{SourceId}}", extImpSmarthub.getToken()); } @@ -84,9 +85,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/smartrtb/model/SmartrtbResponseExt.java b/src/main/java/org/prebid/server/bidder/smartrtb/model/SmartrtbResponseExt.java index b31fd6d012b..40c14ce85cf 100644 --- a/src/main/java/org/prebid/server/bidder/smartrtb/model/SmartrtbResponseExt.java +++ b/src/main/java/org/prebid/server/bidder/smartrtb/model/SmartrtbResponseExt.java @@ -1,10 +1,8 @@ package org.prebid.server.bidder.smartrtb.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class SmartrtbResponseExt { String format; 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..9b466c5f296 100644 --- a/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java +++ b/src/main/java/org/prebid/server/bidder/smartyads/SmartyAdsBidder.java @@ -37,7 +37,6 @@ public class SmartyAdsBidder implements Bidder { private static final String URL_HOST_MACRO = "{{Host}}"; private static final String URL_SOURCE_ID_MACRO = "{{SourceId}}"; private static final String URL_ACCOUNT_ID_MACRO = "{{AccountID}}"; - private static final int FIRST_SEAT_BID_INDEX = 0; private final String endpointUrl; private final JacksonMapper mapper; @@ -146,7 +145,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..67bfec26571 100644 --- a/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java +++ b/src/main/java/org/prebid/server/bidder/smilewanted/SmileWantedBidder.java @@ -1,11 +1,11 @@ package org.prebid.server.bidder.smilewanted; +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.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; @@ -13,9 +13,13 @@ 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.smilewanted.ExtImpSmilewanted; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import java.util.Collections; @@ -27,6 +31,11 @@ public class SmileWantedBidder implements Bidder { private static final String SW_INTEGRATION_TYPE = "prebid_server"; private static final String X_OPENRTB_VERSION = "2.5"; private static final int DEFAULT_AT = 1; + private static final String ZONE_ID_MACRO = "{{ZoneId}}"; + + private static final TypeReference> SMILEWANTED_EXT_TYPE_REFERENCE = + new TypeReference<>() { + }; private final String endpointUrl; private final JacksonMapper mapper; @@ -38,15 +47,30 @@ public SmileWantedBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { + final ExtImpSmilewanted extImpSmilewanted; + + try { + extImpSmilewanted = parseImpExt(request.getImp().getFirst()); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + final BidRequest outgoingRequest = request.toBuilder().at(DEFAULT_AT).build(); + final String url = endpointUrl.replace(ZONE_ID_MACRO, HttpUtil.encodeUrl(extImpSmilewanted.getZoneId())); - return Result.withValue(HttpRequest.builder() - .method(HttpMethod.POST) - .uri(endpointUrl) - .headers(createHeaders()) - .payload(outgoingRequest) - .body(mapper.encodeToBytes(outgoingRequest)) - .build()); + return Result.withValue(BidderUtil.defaultRequest( + outgoingRequest, + createHeaders(), + url, + mapper)); + } + + private ExtImpSmilewanted parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), SMILEWANTED_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId()); + } } private static MultiMap createHeaders() { @@ -73,7 +97,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/smoot/SmootBidder.java b/src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java new file mode 100644 index 00000000000..ec054d9982d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smoot/SmootBidder.java @@ -0,0 +1,117 @@ +package org.prebid.server.bidder.smoot; + +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.smoot.ExtImpSmoot; +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 SmootBidder implements Bidder { + + private static final TypeReference> SMOOT_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SmootBidder(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()) { + try { + final ExtImpSmoot extImpSmoot = parseImpExt(imp); + final Imp modifiedImp = modifyImp(imp, extImpSmoot); + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(modifiedImp)) + .build(); + httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + private ExtImpSmoot parseImpExt(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), SMOOT_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Error parsing imp.ext: " + e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpSmoot extImpSmoot) { + final SmootImpExt smootImpExt = StringUtils.isNotEmpty(extImpSmoot.getPlacementId()) + ? SmootImpExt.publisher(extImpSmoot.getPlacementId()) + : SmootImpExt.network(extImpSmoot.getEndpointId()); + + return imp.toBuilder() + .ext(mapper.mapper().createObjectNode().set("bidder", mapper.mapper().valueToTree(smootImpExt))) + .build(); + } + + @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("could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java b/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java new file mode 100644 index 00000000000..214b6f37bc9 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/smoot/SmootImpExt.java @@ -0,0 +1,24 @@ +package org.prebid.server.bidder.smoot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SmootImpExt { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; + + public static SmootImpExt publisher(String placementId) { + return SmootImpExt.of("publisher", placementId, null); + } + + public static SmootImpExt network(String endpointId) { + return SmootImpExt.of("network", null, endpointId); + } +} 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..7185d339495 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.getXNative() != null) { + return BidType.xNative; + } return BidType.banner; } } diff --git a/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java b/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java index ac5e2268500..ee79f854777 100644 --- a/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java +++ b/src/main/java/org/prebid/server/bidder/sovrn/SovrnBidder.java @@ -41,7 +41,6 @@ public class SovrnBidder implements Bidder { private static final String LJT_READER_COOKIE_NAME = "ljt_reader"; - private static final String EXT_AD_UNIT_CODE_PARAM = "adunitcode"; private static final TypeReference> SOVRN_EXT_TYPE_REFERENCE = new TypeReference<>() { @@ -91,7 +90,6 @@ private Imp makeImp(Imp imp) { return imp.toBuilder() .bidfloor(resolveBidFloor(imp.getBidfloor(), sovrnExt.getBidfloor())) .tagid(resolveTagId(sovrnExt)) - .ext(resolveImpExt(sovrnExt, impExt)) .build(); } @@ -117,13 +115,6 @@ private String resolveTagId(ExtImpSovrn sovrnExt) { return tagId; } - private ObjectNode resolveImpExt(ExtImpSovrn sovrnExt, ObjectNode impExt) { - final ObjectNode sovrnImpExt = impExt.deepCopy(); - return StringUtils.isNotBlank(sovrnExt.getAdunitcode()) - ? sovrnImpExt.putPOJO(EXT_AD_UNIT_CODE_PARAM, sovrnExt.getAdunitcode()) - : sovrnImpExt; - } - private Result>> makeHttpRequest(BidRequest bidRequest, List errors) { diff --git a/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java new file mode 100644 index 00000000000..42c12a48d1c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java @@ -0,0 +1,316 @@ +package org.prebid.server.bidder.sparteo; + +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.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.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.sparteo.util.SparteoUtil; +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.sparteo.ExtImpSparteo; +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 org.apache.http.client.utils.URIBuilder; + +import java.net.URISyntaxException; +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 SparteoBidder implements Bidder { + + private static final String UNKNOWN_VALUE = "unknown"; + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { }; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public SparteoBidder(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<>(); + String networkId = null; + + for (Imp imp : request.getImp()) { + if (networkId == null) { + try { + networkId = parseExtImp(imp).getNetworkId(); + } catch (PreBidException e) { + errors.add(BidderError.badInput( + "ignoring imp id=%s, error processing ext: %s".formatted( + imp.getId(), e.getMessage()))); + } + } + final ObjectNode modifiedExt = modifyImpExt(imp); + modifiedImps.add(imp.toBuilder().ext(modifiedExt).build()); + } + + if (modifiedImps.isEmpty()) { + return Result.withErrors(errors); + } + + final Site site = request.getSite(); + final App app = request.getApp(); + + final BidRequest outgoingRequest = request.toBuilder() + .imp(modifiedImps) + .site(modifySite(site, networkId)) + .app(modifyApp(app, networkId)) + .build(); + + final String finalEndpointUrl = replaceMacros(site, app, networkId, errors); + final HttpRequest call = BidderUtil.defaultRequest(outgoingRequest, finalEndpointUrl, mapper); + + return Result.of(Collections.singletonList(call), errors); + } + + private ExtImpSparteo parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid imp.ext"); + } + } + + private static ObjectNode modifyImpExt(Imp imp) { + final ObjectNode modifiedImpExt = imp.getExt().deepCopy(); + final ObjectNode sparteoNode = modifiedImpExt.putObject("sparteo"); + final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder"); + sparteoNode.set("params", bidderJsonNode); + return modifiedImpExt; + } + + private Site modifySite(Site site, String networkId) { + if (site == null) { + return site; + } + + final Publisher originalPublisher = site.getPublisher() != null + ? site.getPublisher() + : Publisher.builder().build(); + + final Publisher modifiedPublisher = modifyPublisher(originalPublisher, networkId); + + return site.toBuilder().publisher(modifiedPublisher).build(); + } + + private App modifyApp(App app, String networkId) { + if (app == null) { + return app; + } + + final Publisher originalPublisher = app.getPublisher() != null + ? app.getPublisher() + : Publisher.builder().build(); + + final Publisher modifiedPublisher = modifyPublisher(originalPublisher, networkId); + + return app.toBuilder().publisher(modifiedPublisher).build(); + } + + private Publisher modifyPublisher(Publisher originalPublisher, String networkId) { + final ExtPublisher originalExt = originalPublisher.getExt(); + final ExtPublisher modifiedExt = originalExt == null + ? ExtPublisher.empty() + : mapper.mapper().convertValue(originalExt, ExtPublisher.class); + + final ObjectNode paramsNode = ensureParamsNode(modifiedExt); + paramsNode.put("networkId", networkId); + + return originalPublisher.toBuilder() + .ext(modifiedExt) + .build(); + } + + private ObjectNode ensureParamsNode(ExtPublisher extPublisher) { + final JsonNode paramsProperty = extPublisher.getProperty("params"); + if (paramsProperty != null && paramsProperty.isObject()) { + return (ObjectNode) paramsProperty; + } + final ObjectNode paramsNode = mapper.mapper().createObjectNode(); + extPublisher.addProperty("params", paramsNode); + + return paramsNode; + } + + private String replaceMacros(Site site, App app, String networkId, List errors) { + final String siteDomain = resolveSiteDomain(site); + final String appDomain = resolveAppDomain(app); + final String bundle = resolveBundle(app); + + if (UNKNOWN_VALUE.equals(siteDomain)) { + errors.add(BidderError.badInput( + "Domain not found. Missing the site.domain or the site.page field")); + } + + if (UNKNOWN_VALUE.equals(bundle)) { + errors.add(BidderError.badInput( + "Bundle not found. Missing the app.bundle field.")); + } + + return resolveEndpoint(siteDomain, appDomain, networkId, bundle); + } + + private String resolveSiteDomain(Site site) { + if (site == null) { + return null; + } + + return Optional.of(site) + .map(Site::getDomain) + .map(SparteoUtil::normalizeHostname) + .filter(StringUtils::isNotEmpty) + .or(() -> Optional.ofNullable(site.getPage()) + .map(SparteoUtil::normalizeHostname) + .filter(StringUtils::isNotEmpty)) + .orElse(UNKNOWN_VALUE); + } + + private String resolveAppDomain(App app) { + if (app == null) { + return null; + } + + return Optional.of(app) + .map(App::getDomain) + .map(SparteoUtil::normalizeHostname) + .filter(StringUtils::isNotEmpty) + .orElse(UNKNOWN_VALUE); + } + + private String resolveBundle(App app) { + if (app == null) { + return null; + } + + return Optional.ofNullable(app.getBundle()) + .filter(StringUtils::isNotBlank) + .map(String::trim) + .filter(bundle -> !"null".equalsIgnoreCase(bundle)) + .orElse(UNKNOWN_VALUE); + } + + private String resolveEndpoint(String siteDomain, String appDomain, String networkId, String bundle) { + try { + final URIBuilder uriBuilder = new URIBuilder(endpointUrl); + if (StringUtils.isNotBlank(networkId)) { + uriBuilder.addParameter("network_id", networkId); + } + if (StringUtils.isNotBlank(siteDomain)) { + uriBuilder.addParameter("site_domain", siteDomain); + } + if (StringUtils.isNotBlank(appDomain)) { + uriBuilder.addParameter("app_domain", appDomain); + } + if (StringUtils.isNotBlank(bundle)) { + uriBuilder.addParameter("bundle", bundle); + } + return uriBuilder.build().toString(); + } catch (URISyntaxException e) { + throw new PreBidException("Failed to build endpoint URL", e); + } + } + + @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 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 { + final BidType bidType = getBidType(bid); + + final Integer mtype = switch (bidType) { + case banner -> 1; + case video -> 2; + case xNative -> 4; + default -> null; + }; + + final Bid bidWithMtype = mtype != null ? bid.toBuilder().mtype(mtype).build() : bid; + + return BidderBid.of(bidWithMtype, bidType, currency); + } catch (PreBidException e) { + errors.add(BidderError.badServerResponse(e.getMessage())); + return null; + } + } + + private BidType getBidType(Bid bid) throws PreBidException { + final BidType bidType = Optional.ofNullable(bid.getExt()) + .map(ext -> ext.get("prebid")) + .filter(JsonNode::isObject) + .map(this::parseExtBidPrebid) + .map(ExtBidPrebid::getType) + .orElseThrow(() -> new PreBidException( + "Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid()))); + + if (bidType == BidType.audio) { + throw new PreBidException( + "Audio bid type not supported by this adapter for impression id: %s".formatted(bid.getImpid())); + } + + return bidType; + } + + private ExtBidPrebid parseExtBidPrebid(JsonNode prebidNode) { + try { + return mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class); + } catch (JsonProcessingException e) { + return null; + } + } +} diff --git a/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java b/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java new file mode 100644 index 00000000000..bcb098f687d --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sparteo/util/SparteoUtil.java @@ -0,0 +1,41 @@ +package org.prebid.server.bidder.sparteo.util; + +import org.apache.commons.lang3.StringUtils; + +import java.net.URI; +import java.net.URISyntaxException; + +public final class SparteoUtil { + + private SparteoUtil() { + } + + public static String normalizeHostname(String host) { + String h = StringUtils.trimToEmpty(host); + if (h.isEmpty()) { + return ""; + } + + String hostname = null; + try { + hostname = new URI(h).getHost(); + } catch (URISyntaxException e) { + } + + if (StringUtils.isNotEmpty(hostname)) { + h = hostname; + } else { + if (h.contains(":")) { + h = StringUtils.substringBefore(h, ":"); + } else { + h = StringUtils.substringBefore(h, "/"); + } + } + + h = h.toLowerCase(); + h = StringUtils.removeStart(h, "www."); + h = StringUtils.removeEnd(h, "."); + + return "null".equals(h) ? "" : h; + } +} diff --git a/src/main/java/org/prebid/server/bidder/sspbc/SspbcBidder.java b/src/main/java/org/prebid/server/bidder/sspbc/SspbcBidder.java index c84a857533a..47419fb21a7 100644 --- a/src/main/java/org/prebid/server/bidder/sspbc/SspbcBidder.java +++ b/src/main/java/org/prebid/server/bidder/sspbc/SspbcBidder.java @@ -1,18 +1,11 @@ package org.prebid.server.bidder.sspbc; -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.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.http.HttpMethod; 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; import org.prebid.server.bidder.model.BidderBid; @@ -20,47 +13,22 @@ 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.sspbc.request.SspbcRequestType; 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.sspbc.ExtImpSspbc; 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.net.URISyntaxException; -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.Optional; -import java.util.function.Function; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; -public class SspbcBidder implements Bidder { +public class SspbcBidder implements Bidder { - private static final TypeReference> SSPBC_EXT_TYPE_REFERENCE = - new TypeReference<>() { - }; - private static final String ADAPTER_VERSION = "5.8"; - private static final String IMP_FALLBACK_SIZE = "1x1"; - private static final String PREBID_SERVER_INTEGRATION_TYPE = "4"; - private static final String BANNER_TEMPLATE = """ -
"""; + private static final String ADAPTER_VERSION = "6.0"; private final String endpointUrl; private final JacksonMapper mapper; @@ -71,247 +39,66 @@ public SspbcBidder(String endpointUrl, JacksonMapper mapper) { } @Override - public Result>> makeHttpRequests(BidRequest request) { - if (request.getSite() == null) { - return Result.withError(BidderError.badInput("BidRequest.site not provided")); - } - - final Map impToExt; - - try { - impToExt = createImpToExt(request); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - - final SspbcRequestType requestType = getRequestType(impToExt); - final List imps = new ArrayList<>(); - String siteId = ""; - - for (Imp imp : request.getImp()) { - final ExtImpSspbc extImpSspbc = impToExt.get(imp); - final String extImpSspbcSiteId = extImpSspbc.getSiteId(); - if (StringUtils.isNotEmpty(extImpSspbcSiteId)) { - siteId = extImpSspbcSiteId; - } - - imps.add(updateImp(imp, requestType, extImpSspbc)); - } - - try { - return Result.withValue(createRequest(updateBidRequest(request, imps, requestType, siteId))); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } - } - - private Map createImpToExt(BidRequest request) { - return request.getImp() - .stream() - .collect(Collectors.toMap(Function.identity(), this::parseImpExt)); - } - - private ExtImpSspbc parseImpExt(Imp imp) { - try { - return mapper.mapper().convertValue(imp.getExt(), SSPBC_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - throw new PreBidException(e.getMessage()); - } - } - - private Imp updateImp(Imp imp, SspbcRequestType requestType, ExtImpSspbc extImpSspbc) { - final String originalImpId = imp.getId(); - - return imp.toBuilder() - .id(resolveImpId(originalImpId, extImpSspbc.getId(), requestType)) - .tagid(originalImpId) - .ext(createImpExt(imp)) + public Result>> makeHttpRequests(BidRequest request) { + return Result.withValue(createHttpRequest(request)); + } + + private HttpRequest createHttpRequest(BidRequest request) { + final SspbcRequest outgoingRequest = SspbcRequest.of(request); + return HttpRequest.builder() + .method(HttpMethod.POST) + .uri(makeUrl(endpointUrl)) + .headers(HttpUtil.headers()) + .impIds(BidderUtil.impIds(outgoingRequest.getBidRequest())) + .body(mapper.encodeToBytes(outgoingRequest)) + .payload(outgoingRequest) .build(); } - private String resolveImpId(String originalImpId, String extImpId, SspbcRequestType requestType) { - return StringUtils.isNotEmpty(extImpId) && requestType != SspbcRequestType.REQUEST_TYPE_ONE_CODE - ? extImpId - : originalImpId; - } - - private ObjectNode createImpExt(Imp imp) { - return mapper.mapper().createObjectNode() - .set("data", mapper.mapper().createObjectNode() - .put("pbslot", imp.getTagid()) - .put("pbsize", getImpSize(imp))); - } - - private BidRequest updateBidRequest(BidRequest bidRequest, - List imps, - SspbcRequestType requestType, - String siteId) { - - return bidRequest.toBuilder() - .imp(imps) - .site(updateSite(bidRequest.getSite(), requestType, siteId)) - .test(updateTest(requestType, bidRequest.getTest())) - .build(); - } - - private Site updateSite(Site site, SspbcRequestType requestType, String siteId) { - return site.toBuilder() - .id(resolveSiteId(requestType, siteId)) - .domain(getUri(site.getPage()).getHost()) - .build(); - } - - private static String resolveSiteId(SspbcRequestType requestType, String siteId) { - return requestType == SspbcRequestType.REQUEST_TYPE_ONE_CODE || StringUtils.isBlank(siteId) - ? StringUtils.EMPTY - : siteId; - } - - private static Integer updateTest(SspbcRequestType requestType, Integer test) { - return requestType == SspbcRequestType.REQUEST_TYPE_TEST ? 1 : test; - } - - private String getImpSize(Imp imp) { - final List formats = ObjectUtil.getIfNotNull(imp.getBanner(), Banner::getFormat); - if (CollectionUtils.isEmpty(formats)) { - return IMP_FALLBACK_SIZE; - } - - return formats.stream() - .max(Comparator.comparing(SspbcBidder::formatToArea)) - .filter(format -> formatToArea(format) > 0) - .map(format -> String.format("%dx%d", format.getW(), format.getH())) - .orElse(IMP_FALLBACK_SIZE); - } - - private static int formatToArea(Format format) { - final Integer w = ObjectUtil.getIfNotNull(format, Format::getW); - final Integer h = ObjectUtil.getIfNotNull(format, Format::getH); - - return w != null && h != null ? w * h : 0; - } - - private SspbcRequestType getRequestType(Map impToExt) { - for (ExtImpSspbc extImpSspbc : impToExt.values()) { - if (extImpSspbc.getTest() != 0) { - return SspbcRequestType.REQUEST_TYPE_TEST; - } - - if (StringUtils.isAnyEmpty(extImpSspbc.getSiteId(), extImpSspbc.getId())) { - return SspbcRequestType.REQUEST_TYPE_ONE_CODE; - } - } - - return SspbcRequestType.REQUEST_TYPE_STANDARD; - } - - private HttpRequest createRequest(BidRequest request) { - return BidderUtil.defaultRequest(request, updateUrl(getUri(endpointUrl)), mapper); - } - - private static URIBuilder getUri(String endpointUrl) { - final URIBuilder uri; + private static String makeUrl(String endpointUrl) { try { - uri = new URIBuilder(endpointUrl); + return new URIBuilder(endpointUrl) + .addParameter("bdver", ADAPTER_VERSION) + .build() + .toString(); } catch (URISyntaxException e) { throw new PreBidException("Malformed URL: %s.".formatted(endpointUrl)); } - return uri; - } - - private String updateUrl(URIBuilder uriBuilder) { - return uriBuilder - .addParameter("bdver", ADAPTER_VERSION) - .addParameter("inver", PREBID_SERVER_INTEGRATION_TYPE) - .toString(); } @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + 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())); + return Result.withValues(extractBids(bidResponse)); } catch (PreBidException | DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidResponse bidResponse, BidRequest bidRequest) { + private List extractBids(BidResponse bidResponse) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) - .map(seatBid -> CollectionUtils.emptyIfNull(seatBid.getBid()) - .stream() - .filter(Objects::nonNull) - .map(bid -> toBidderBid(bid, seatBid.getSeat(), bidResponse.getCur(), bidRequest))) - .flatMap(UnaryOperator.identity()) + .map(SeatBid::getBid) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) .toList(); } - private BidderBid toBidderBid(Bid bid, String seat, String currency, BidRequest bidRequest) { - if (StringUtils.isEmpty(bid.getAdm())) { - throw new PreBidException("Bid format is not supported"); - } - - final ObjectNode bidExt = bid.getExt(); - final Bid updatedBid = bid.toBuilder() - .impid(getImpTagId(bidRequest.getImp(), bid)) - .adm(createBannerAd( - bid, - stringOrNull(bidExt, "adlabel"), - stringOrNull(bidExt, "pubid"), - stringOrNull(bidExt, "siteid"), - stringOrNull(bidExt, "slotid"), - seat, - bidRequest)) - .build(); - - return BidderBid.of(updatedBid, BidType.banner, currency); - } - - private static String stringOrNull(ObjectNode bidExt, String property) { - return Optional.ofNullable(bidExt) - .map(ext -> ext.get(property)) - .map(JsonNode::asText) - .orElse(StringUtils.EMPTY); - } - - private static String getImpTagId(List imps, Bid bid) { - return imps.stream() - .filter(imp -> Objects.equals(imp.getId(), bid.getImpid())) - .map(Imp::getTagid) - .findAny() - .orElseThrow(() -> new PreBidException("imp not found")); - } - - private String createBannerAd(Bid bid, - String adlabel, - String pubid, - String siteid, - String slotid, - String seat, - BidRequest bidRequest) { - if (bid.getAdm().contains("")) { - return bid.getAdm(); - } - - final ObjectNode mcad = mapper.mapper().createObjectNode() - .put("id", bidRequest.getId()) - .put("seat", seat) - .set("seatbid", mapper.mapper() - .convertValue(SeatBid.builder().bid(Collections.singletonList(bid)).build(), JsonNode.class)); - - return BANNER_TEMPLATE - .replace("{{.SiteId}}", siteid) - .replace("{{.SlotId}}", slotid) - .replace("{{.AdLabel}}", adlabel) - .replace("{{.PubId}}", pubid) - .replace("{{.Page}}", bidRequest.getSite().getPage()) - .replace("{{.Referer}}", bidRequest.getSite().getRef()) - .replace("{{.McAd}}", mcad.toString()) - .replace("{{.Inver}}", PREBID_SERVER_INTEGRATION_TYPE); + private static BidType getBidType(Bid bid) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null -> throw new PreBidException("Bid mtype is required"); + default -> throw new PreBidException("unsupported MType: %s.".formatted(bid.getMtype())); + }; } } diff --git a/src/main/java/org/prebid/server/bidder/sspbc/SspbcRequest.java b/src/main/java/org/prebid/server/bidder/sspbc/SspbcRequest.java new file mode 100644 index 00000000000..5e0859a6c98 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/sspbc/SspbcRequest.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.sspbc; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.request.BidRequest; +import lombok.Value; + +@Value(staticConstructor = "of") +public class SspbcRequest { + + @JsonProperty("bidRequest") + BidRequest bidRequest; +} diff --git a/src/main/java/org/prebid/server/bidder/sspbc/request/SspbcRequestType.java b/src/main/java/org/prebid/server/bidder/sspbc/request/SspbcRequestType.java deleted file mode 100644 index b78b4abbdb3..00000000000 --- a/src/main/java/org/prebid/server/bidder/sspbc/request/SspbcRequestType.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.bidder.sspbc.request; - -public enum SspbcRequestType { - - REQUEST_TYPE_STANDARD(1), - REQUEST_TYPE_ONE_CODE(2), - REQUEST_TYPE_TEST(3); - - private final Integer value; - - SspbcRequestType(Integer value) { - this.value = value; - } - - public Integer getValue() { - return value; - } -} diff --git a/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java b/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java new file mode 100644 index 00000000000..e7ce856a4bc --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/startio/StartioBidder.java @@ -0,0 +1,149 @@ +package org.prebid.server.bidder.startio; + +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +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.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.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 StartioBidder implements Bidder { + + private static final JsonPointer BID_TYPE_POINTER = JsonPointer.valueOf("/prebid/type"); + private static final String BID_CURRENCY = "USD"; + + private final String endpointUrl; + private final JacksonMapper mapper; + + public StartioBidder(String endpointUrl, JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public final Result>> makeHttpRequests(BidRequest bidRequest) { + if (hasNoAppOrSiteId(bidRequest)) { + return Result.withError(BidderError.badInput( + "Bidder requires either app.id or site.id to be specified.")); + } + + if (isSupportedCurrency(bidRequest.getCur())) { + return Result.withError(BidderError.badInput("Unsupported currency, bidder only accepts USD.")); + } + + final List> requests = new ArrayList<>(); + final List errors = new ArrayList<>(); + final List imps = bidRequest.getImp(); + for (int i = 0; i < imps.size(); i++) { + final Imp imp = imps.get(i); + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { + errors.add(BidderError.badInput( + "imp[%d]: Unsupported media type, bidder does not support audio.".formatted(i))); + + continue; + } + + final Imp modifiedImp = imp.getAudio() != null + ? imp.toBuilder().audio(null).build() + : imp; + requests.add(BidderUtil.defaultRequest( + bidRequest.toBuilder().imp(Collections.singletonList(modifiedImp)).build(), + endpointUrl, mapper)); + } + + return Result.of(requests, errors); + } + + private static boolean hasNoAppOrSiteId(BidRequest bidRequest) { + final App app = bidRequest.getApp(); + final Site site = bidRequest.getSite(); + return (app == null || StringUtils.isEmpty(app.getId())) + && (site == null || StringUtils.isEmpty(site.getId())); + } + + private static boolean isSupportedCurrency(List currencies) { + return CollectionUtils.isNotEmpty(currencies) && !currencies.contains(BID_CURRENCY); + } + + @Override + public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final BidResponse response; + + try { + response = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + } catch (DecodeException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); + } + + final List errors = new ArrayList<>(); + return Result.of(extractBids(response, errors), errors); + } + + 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) + .filter(Objects::nonNull) + .map(bid -> constructBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid constructBidderBid(Bid bid, String currency, List errors) { + final BidType type = getBidType(bid); + if (type != null) { + return BidderBid.of(bid, type, currency); + } + + errors.add(BidderError.badServerResponse( + "Failed to parse bid media type for impression %s.".formatted(bid.getImpid()))); + return null; + } + + private static BidType getBidType(Bid bid) { + final JsonNode ext = bid.getExt(); + final JsonNode bidType = ext != null ? ext.at(BID_TYPE_POINTER) : null; + if (bidType == null || !bidType.isTextual()) { + return null; + } + + return switch (bidType.textValue()) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + case "native" -> BidType.xNative; + default -> null; + }; + } + +} diff --git a/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java b/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java index a3d02f5250d..fea452f059d 100644 --- a/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java +++ b/src/main/java/org/prebid/server/bidder/stroeercore/StroeerCoreBidder.java @@ -6,17 +6,14 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; 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.stroeercore.model.StroeerCoreBid; import org.prebid.server.bidder.stroeercore.model.StroeerCoreBidResponse; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -26,7 +23,6 @@ import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -41,14 +37,10 @@ public class StroeerCoreBidder implements Bidder { private final String endpointUrl; private final JacksonMapper mapper; - private final CurrencyConversionService currencyConversionService; - public StroeerCoreBidder(String endpointUrl, - JacksonMapper mapper, - CurrencyConversionService currencyConversionService) { + public StroeerCoreBidder(String endpointUrl, JacksonMapper mapper) { this.endpointUrl = HttpUtil.validateUrl(endpointUrl); this.mapper = Objects.requireNonNull(mapper); - this.currencyConversionService = Objects.requireNonNull(currencyConversionService); } @Override @@ -57,22 +49,12 @@ public Result>> makeHttpRequests(BidRequest bidRequ final List errors = new ArrayList<>(); for (Imp imp : bidRequest.getImp()) { - final ExtImpStroeerCore impExt; - final Price price; - try { - validateImp(imp); - - impExt = parseImpExt(imp); - validateImpExt(impExt); - - price = convertBidFloor(bidRequest, imp); + final ExtImpStroeerCore impExt = parseImpExt(imp); + modifiedImps.add(imp.toBuilder().tagid(impExt.getSlotId()).build()); } catch (PreBidException e) { errors.add(BidderError.badInput("%s. Ignore imp id = %s.".formatted(e.getMessage(), imp.getId()))); - continue; } - - modifiedImps.add(modifyImp(imp, impExt, price)); } if (modifiedImps.isEmpty()) { @@ -80,14 +62,7 @@ public Result>> makeHttpRequests(BidRequest bidRequ } final BidRequest outgoingRequest = bidRequest.toBuilder().imp(modifiedImps).build(); - - return createHttpRequests(errors, outgoingRequest); - } - - private static void validateImp(Imp imp) { - if (imp.getBanner() == null && imp.getVideo() == null) { - throw new PreBidException("Expected banner or video impression"); - } + return Result.withValue(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); } private ExtImpStroeerCore parseImpExt(Imp imp) { @@ -98,95 +73,73 @@ private ExtImpStroeerCore parseImpExt(Imp imp) { } } - private static void validateImpExt(ExtImpStroeerCore impExt) { - if (StringUtils.isBlank(impExt.getSlotId())) { - throw new PreBidException("Custom param slot id (sid) is empty"); - } - } - - private Price convertBidFloor(BidRequest bidRequest, Imp imp) { - final BigDecimal bidFloor = imp.getBidfloor(); - final String bidFloorCurrency = imp.getBidfloorcur(); - - if (!shouldConvertBidFloor(bidFloor, bidFloorCurrency)) { - return Price.of(bidFloorCurrency, bidFloor); - } - - final BigDecimal convertedBidFloor = currencyConversionService.convertCurrency( - bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY); - - return Price.of(BIDDER_CURRENCY, convertedBidFloor); - } - - private Result>> createHttpRequests(List errors, BidRequest bidRequest) { - return Result.of(Collections.singletonList(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)), errors); - } - - private static boolean shouldConvertBidFloor(BigDecimal bidFloor, String bidFloorCurrency) { - return BidderUtil.isValidPrice(bidFloor) && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY); - } - - private static Imp modifyImp(Imp imp, ExtImpStroeerCore impExt, Price price) { - return imp.toBuilder() - .bidfloorcur(price.getCurrency()) - .bidfloor(price.getValue()) - .tagid(impExt.getSlotId()) - .build(); - } - @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { final String body = httpCall.getResponse().getBody(); + final List errors = new ArrayList<>(); final StroeerCoreBidResponse bidResponse = mapper.decodeValue(body, StroeerCoreBidResponse.class); - return Result.withValues(extractBids(httpCall.getRequest().getPayload(), bidResponse)); + return Result.of(extractBids(bidResponse, errors), errors); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private List extractBids(BidRequest bidRequest, StroeerCoreBidResponse bidResponse) { + private List extractBids(StroeerCoreBidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getBids())) { return Collections.emptyList(); } return bidResponse.getBids().stream() .filter(Objects::nonNull) - .map(stroeerCoreBid -> toBidderBid(bidRequest, stroeerCoreBid)) + .map(stroeerCoreBid -> toBidderBid(stroeerCoreBid, errors)) + .filter(Objects::nonNull) .toList(); } - private BidderBid toBidderBid(BidRequest bidRequest, StroeerCoreBid stroeercoreBid) { - final ObjectNode bidExt = stroeercoreBid.getDsa() != null - ? mapper.mapper().createObjectNode().set("dsa", stroeercoreBid.getDsa()) - : null; + private BidderBid toBidderBid(StroeerCoreBid stroeercoreBid, List errors) { + final BidType bidType = getBidType(stroeercoreBid.getMtype()); + if (bidType == null) { + errors.add(BidderError.badServerResponse( + "Bid media type error: unable to determine media type for bid with id \"%s\"" + .formatted(stroeercoreBid.getBidId()))); + return null; + } return BidderBid.of( Bid.builder() .id(stroeercoreBid.getId()) - .impid(stroeercoreBid.getImpId()) + .impid(stroeercoreBid.getBidId()) .w(stroeercoreBid.getWidth()) .h(stroeercoreBid.getHeight()) .price(stroeercoreBid.getCpm()) .adm(stroeercoreBid.getAdMarkup()) .crid(stroeercoreBid.getCreativeId()) - .ext(bidExt) + .adomain(stroeercoreBid.getAdomain()) + .mtype(bidType.ordinal() + 1) + .ext(getBidExt(stroeercoreBid)) .build(), - getBidType(stroeercoreBid.getImpId(), bidRequest.getImp()), + bidType, BIDDER_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; - } else if (imp.getVideo() != null) { - return BidType.video; - } - } + private ObjectNode getBidExt(StroeerCoreBid stroeercoreBid) { + final ObjectNode dsa = stroeercoreBid.getDsa(); + ObjectNode ext = stroeercoreBid.getExt(); + if (dsa == null) { + return ext; + } + if (ext == null) { + ext = mapper.mapper().createObjectNode(); } + return ext.set("dsa", dsa); + } - return BidType.banner; + private static BidType getBidType(String mtype) { + return switch (mtype) { + case "banner" -> BidType.banner; + case "video" -> BidType.video; + default -> null; + }; } } diff --git a/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java b/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java index e44b85f445e..980db6d1891 100644 --- a/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java +++ b/src/main/java/org/prebid/server/bidder/stroeercore/model/StroeerCoreBid.java @@ -6,6 +6,7 @@ import lombok.Value; import java.math.BigDecimal; +import java.util.List; @Value @Builder @@ -14,7 +15,7 @@ public class StroeerCoreBid { String id; @JsonProperty("bidId") - String impId; + String bidId; BigDecimal cpm; @@ -28,6 +29,15 @@ public class StroeerCoreBid { @JsonProperty("crid") String creativeId; + /** + * @deprecated The dsa will move to the bid's ext. + */ + @Deprecated(forRemoval = true) ObjectNode dsa; -} + String mtype; + + List adomain; + + ObjectNode ext; +} 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..67224479693 100644 --- a/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java +++ b/src/main/java/org/prebid/server/bidder/tappx/TappxBidder.java @@ -36,7 +36,7 @@ public class TappxBidder implements Bidder { - private static final String VERSION = "1.4"; + private static final String VERSION = "1.6"; private static final String TYPE_CNN = "prebid"; private static final TypeReference> TAPX_EXT_TYPE_REFERENCE = @@ -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..1e4e9804e11 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(); } } @@ -170,4 +170,3 @@ private ExtBidPrebid parseExtBidPrebidMeta(Bid bid) { } } - 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/telaria/model/TelariaRequestExt.java b/src/main/java/org/prebid/server/bidder/telaria/model/TelariaRequestExt.java index 9841702cf4e..dec3c36c5f7 100644 --- a/src/main/java/org/prebid/server/bidder/telaria/model/TelariaRequestExt.java +++ b/src/main/java/org/prebid/server/bidder/telaria/model/TelariaRequestExt.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.telaria.model; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class TelariaRequestExt { ObjectNode extra; diff --git a/src/main/java/org/prebid/server/bidder/teqblaze/TeqblazeBidder.java b/src/main/java/org/prebid/server/bidder/teqblaze/TeqblazeBidder.java new file mode 100644 index 00000000000..67401fecf12 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/teqblaze/TeqblazeBidder.java @@ -0,0 +1,141 @@ +package org.prebid.server.bidder.teqblaze; + +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.teqblaze.proto.TeqblazeExtImp; +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.teqblaze.ExtImpTeqblaze; +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 TeqblazeBidder implements Bidder { + + private static final TypeReference> EXT_TYPE_REF = 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 TeqblazeBidder(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 ExtImpTeqblaze extImpTeqblaze = parseExtImp(imp); + final Imp modifiedImp = modifyImp(imp, extImpTeqblaze); + httpRequests.add(makeHttpRequest(request, modifiedImp)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return httpRequests.isEmpty() + ? Result.withErrors(errors) + : Result.of(httpRequests, errors); + } + + private ExtImpTeqblaze parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REF).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Cannot deserialize ExtImpTeqblaze: " + e.getMessage()); + } + } + + private Imp modifyImp(Imp imp, ExtImpTeqblaze extImpTeqblaze) { + final TeqblazeExtImp impExtTeqblazeWithType = resolveImpExt(extImpTeqblaze); + final ObjectNode modifiedImpExtBidder = mapper.mapper().createObjectNode(); + modifiedImpExtBidder.set(BIDDER_PROPERTY, mapper.mapper().valueToTree(impExtTeqblazeWithType)); + + return imp.toBuilder().ext(modifiedImpExtBidder).build(); + } + + private TeqblazeExtImp resolveImpExt(ExtImpTeqblaze extImpTeqblaze) { + final TeqblazeExtImp.TeqblazeExtImpBuilder builder = TeqblazeExtImp.builder(); + + if (StringUtils.isNotEmpty(extImpTeqblaze.getPlacementId())) { + builder.type(PUBLISHER_PROPERTY).placementId(extImpTeqblaze.getPlacementId()); + } else if (StringUtils.isNotEmpty(extImpTeqblaze.getEndpointId())) { + builder.type(NETWORK_PROPERTY).endpointId(extImpTeqblaze.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) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 3 -> BidType.audio; + case 4 -> BidType.xNative; + case null, default -> + throw new PreBidException("could not define media type for impression: " + bid.getImpid()); + }; + } +} diff --git a/src/main/java/org/prebid/server/bidder/teqblaze/proto/TeqblazeExtImp.java b/src/main/java/org/prebid/server/bidder/teqblaze/proto/TeqblazeExtImp.java new file mode 100644 index 00000000000..d262e107bdd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/teqblaze/proto/TeqblazeExtImp.java @@ -0,0 +1,18 @@ +package org.prebid.server.bidder.teqblaze.proto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class TeqblazeExtImp { + + String type; + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} 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..a2853b26d72 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/thetradedesk/TheTradeDeskBidder.java @@ -0,0 +1,237 @@ +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 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.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.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.regex.Pattern; + +public class TheTradeDeskBidder implements Bidder { + + private static final TypeReference> TYPE_REFERENCE = + new TypeReference<>() { + }; + + private static final String SUPPLY_ID_MACRO = "{{SupplyId}}"; + private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$"); + private static final String PRICE_MACRO = "${AUCTION_PRICE}"; + + 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; + String sourceSupplyId = null; + for (Imp imp : request.getImp()) { + try { + final ExtImpTheTradeDesk extImp = parseImpExt(imp); + + final String extImpPublisherId = extImp.getPublisherId(); + publisherId = publisherId == null && StringUtils.isNotBlank(extImpPublisherId) + ? extImpPublisherId + : publisherId; + + final String extImpSourceSupplyId = extImp.getSupplySourceId(); + sourceSupplyId = sourceSupplyId == null && StringUtils.isNotBlank(extImpSourceSupplyId) + ? extImpSourceSupplyId + : sourceSupplyId; + + modifiedImps.add(modifyImp(imp)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + if (StringUtils.isBlank(sourceSupplyId) && StringUtils.isBlank(supplyId)) { + return Result.withError( + BidderError.badInput("Either supplySourceId or a default endpoint must be provided")); + } + + final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId); + final HttpRequest httpRequest = BidderUtil.defaultRequest( + outgoingRequest, + resolveEndpoint(sourceSupplyId), + 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(String sourceSupplyId) { + return endpointUrl.replace( + SUPPLY_ID_MACRO, + HttpUtil.encodeUrl(StringUtils.defaultString(ObjectUtils.defaultIfNull(sourceSupplyId, supplyId)))); + } + + @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 bids = extractBids(bidResponse, errors); + return Result.of(bids, 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 -> makeBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .toList(); + } + + private static BidderBid makeBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null ? BidderBid.of(resolvePriceMacros(bid), bidType, currency) : null; + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse( + "could not define media type for impression: " + bid.getImpid())); + yield null; + } + }; + } + + private static Bid resolvePriceMacros(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(); + } +} diff --git a/src/main/java/org/prebid/server/bidder/thirtythreeacross/ThirtyThreeAcrossBidder.java b/src/main/java/org/prebid/server/bidder/thirtythreeacross/ThirtyThreeAcrossBidder.java index 30eaa0c9035..76642c100b7 100644 --- a/src/main/java/org/prebid/server/bidder/thirtythreeacross/ThirtyThreeAcrossBidder.java +++ b/src/main/java/org/prebid/server/bidder/thirtythreeacross/ThirtyThreeAcrossBidder.java @@ -165,7 +165,6 @@ private static Video updatedVideo(Video video, String productId) { return video.toBuilder() .startdelay(resolveStartDelay(video.getStartdelay(), productId)) - .placement(resolvePlacement(video.getPlacement(), productId)) .build(); } @@ -173,16 +172,6 @@ private static Integer resolveStartDelay(Integer startDelay, String productId) { return Objects.equals(productId, "instream") ? Integer.valueOf(0) : startDelay; } - private static Integer resolvePlacement(Integer videoPlacement, String productId) { - if (Objects.equals(productId, "instream")) { - return 1; - } - if (BidderUtil.isNullOrZero(videoPlacement)) { - return 2; - } - return videoPlacement; - } - private static String getImpGroupName(ThirtyThreeAcrossImpExt impExt) { final ThirtyThreeAcrossImpExtTtx impExtTtx = impExt.getTtx(); return impExtTtx.getProd() + impExtTtx.getZoneid(); diff --git a/src/main/java/org/prebid/server/bidder/tpmn/TpmnBidder.java b/src/main/java/org/prebid/server/bidder/tpmn/TpmnBidder.java index 956e49646eb..4544fda8cf4 100644 --- a/src/main/java/org/prebid/server/bidder/tpmn/TpmnBidder.java +++ b/src/main/java/org/prebid/server/bidder/tpmn/TpmnBidder.java @@ -107,14 +107,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, - List errors) { + private static List extractBids(BidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -125,12 +123,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) { @@ -153,4 +151,3 @@ private static BidType resolveBidType(Bid bid) throws PreBidException { }; } } - 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/triplelift/model/TripleliftInnerExt.java b/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftInnerExt.java index 0da2cde0c3a..bcdb7f5440c 100644 --- a/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftInnerExt.java +++ b/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftInnerExt.java @@ -1,12 +1,9 @@ package org.prebid.server.bidder.triplelift.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class TripleliftInnerExt { Integer format; } - diff --git a/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftResponseExt.java b/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftResponseExt.java index cb2397b5c86..01a0a7e4297 100644 --- a/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftResponseExt.java +++ b/src/main/java/org/prebid/server/bidder/triplelift/model/TripleliftResponseExt.java @@ -1,12 +1,9 @@ package org.prebid.server.bidder.triplelift.model; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class TripleliftResponseExt { TripleliftInnerExt tripleliftPb; } - 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..f350abec72c 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) { @@ -222,4 +222,3 @@ 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..9648ff08a40 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()); @@ -102,9 +102,9 @@ private Imp modifyImp(Imp imp) { : null; final UnicornImpExt resolvedUnicornImpExt = resolvedPlacementId != null - ? unicornImpExt.toBuilder() - .bidder(extImpBidder.toBuilder().placementId(resolvedPlacementId).build()) - .build() + ? UnicornImpExt.of( + unicornImpExt.getContext(), + extImpBidder.toBuilder().placementId(resolvedPlacementId).build()) : null; return imp.toBuilder() diff --git a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java index b4f534d1aad..98915424847 100644 --- a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java +++ b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExt.java @@ -1,13 +1,9 @@ package org.prebid.server.bidder.unicorn.model; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.unicorn.ExtImpUnicorn; -@AllArgsConstructor(staticName = "of") -@Value -@Builder(toBuilder = true) +@Value(staticConstructor = "of") public class UnicornImpExt { UnicornImpExtContext context; diff --git a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java index 05e6f9c36a6..25a8a67d9d6 100644 --- a/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java +++ b/src/main/java/org/prebid/server/bidder/unicorn/model/UnicornImpExtContext.java @@ -1,11 +1,9 @@ package org.prebid.server.bidder.unicorn.model; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class UnicornImpExtContext { ObjectNode data; 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..feac7f16ab0 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; @@ -13,12 +15,13 @@ 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.unruly.proto.UnrulyExtPrebid; 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.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; @@ -30,7 +33,7 @@ public class UnrulyBidder implements Bidder { - private static final TypeReference> UNRULY_EXT_TYPE_REFERENCE = + private static final TypeReference UNRULY_EXT_TYPE_REFERENCE = new TypeReference<>() { }; @@ -54,14 +57,17 @@ public Result>> makeHttpRequests(BidRequest request private Imp modifyImp(Imp imp) { + final UnrulyExtPrebid unrulyExtPrebid = parseImpExt(imp); return imp.toBuilder() - .ext(mapper.mapper().valueToTree(ExtPrebid.of(null, parseImpExt(imp)))) + .ext(mapper.mapper().valueToTree(UnrulyExtPrebid.of( + unrulyExtPrebid.getBidder(), + unrulyExtPrebid.getGpid()))) .build(); } - private ExtImpUnruly parseImpExt(Imp imp) { + private UnrulyExtPrebid parseImpExt(Imp imp) { try { - return mapper.mapper().convertValue(imp.getExt(), UNRULY_EXT_TYPE_REFERENCE).getBidder(); + return mapper.mapper().convertValue(imp.getExt(), UNRULY_EXT_TYPE_REFERENCE); } catch (IllegalArgumentException e) { throw new PreBidException(e.getMessage()); } @@ -84,18 +90,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 +113,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 +145,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/unruly/proto/UnrulyExtPrebid.java b/src/main/java/org/prebid/server/bidder/unruly/proto/UnrulyExtPrebid.java new file mode 100644 index 00000000000..f34ec91894f --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/unruly/proto/UnrulyExtPrebid.java @@ -0,0 +1,12 @@ +package org.prebid.server.bidder.unruly.proto; + +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.request.unruly.ExtImpUnruly; + +@Value(staticConstructor = "of") +public class UnrulyExtPrebid { + + ExtImpUnruly bidder; + + String gpid; +} 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/visx/VisxBidder.java b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java index e7f91b12717..49e64645206 100644 --- a/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java +++ b/src/main/java/org/prebid/server/bidder/visx/VisxBidder.java @@ -1,11 +1,14 @@ package org.prebid.server.bidder.visx; -import com.fasterxml.jackson.databind.JsonNode; +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 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; @@ -18,7 +21,10 @@ 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.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; @@ -26,6 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class VisxBidder implements Bidder { @@ -33,6 +40,9 @@ public class VisxBidder implements Bidder { private static final String DEFAULT_REQUEST_CURRENCY = "USD"; private static final Set SUPPORTED_BID_TYPES_TEXTUAL = Set.of("banner", "video"); + private static final TypeReference> BID_EXT_TYPE_REFERENCE = new TypeReference<>() { + }; + private final String endpointUrl; private final JacksonMapper mapper; @@ -43,20 +53,28 @@ public VisxBidder(String endpointUrl, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - return Result.withValue(makeRequest(request)); - } - - private HttpRequest makeRequest(BidRequest bidRequest) { - final BidRequest outgoingRequest = modifyRequest(bidRequest); - return BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper); + final BidRequest outgoingRequest = modifyRequest(request); + return Result.withValue( + BidderUtil.defaultRequest(outgoingRequest, makeHeaders(request.getDevice()), endpointUrl, mapper)); } - private BidRequest modifyRequest(BidRequest bidRequest) { + private static BidRequest modifyRequest(BidRequest bidRequest) { return CollectionUtils.isEmpty(bidRequest.getCur()) ? bidRequest.toBuilder().cur(Collections.singletonList(DEFAULT_REQUEST_CURRENCY)).build() : bidRequest; } + private static MultiMap makeHeaders(Device device) { + final MultiMap headers = HttpUtil.headers(); + + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); + } + + return headers; + } + @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { try { @@ -80,14 +98,14 @@ private List bidsFromResponse(BidRequest bidRequest, VisxResponse vis .map(VisxSeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(visxBid -> toBidderBid(bidRequest, visxBid)) + .map(visxBid -> toBidderBid(bidRequest, visxBid, visxResponse.getCur())) .toList(); } - private BidderBid toBidderBid(BidRequest bidRequest, VisxBid visxBid) { + private BidderBid toBidderBid(BidRequest bidRequest, VisxBid visxBid, String currency) { final Bid bid = toBid(visxBid, bidRequest.getId()); final BidType bidType = getBidType(bid.getExt(), bid.getImpid(), bidRequest.getImp()); - return BidderBid.of(bid, bidType, null); + return BidderBid.of(bid, bidType, StringUtils.defaultIfBlank(currency, null)); } private static Bid toBid(VisxBid visxBid, String id) { @@ -105,20 +123,24 @@ private static Bid toBid(VisxBid visxBid, String id) { .build(); } - private static BidType getBidType(ObjectNode bidExt, String impId, List imps) { + private BidType getBidType(ObjectNode bidExt, String impId, List imps) { final BidType extBidType = getBidTypeFromExt(bidExt); return extBidType != null ? extBidType : getBidTypeFromImp(impId, imps); } - private static BidType getBidTypeFromExt(ObjectNode bidExt) { - final JsonNode mediaTypeNode = bidExt != null ? bidExt.at("/prebid/meta/mediaType") : null; - final String bidTypeTextual = mediaTypeNode != null && mediaTypeNode.isTextual() - ? mediaTypeNode.asText() - : null; - - return bidTypeTextual != null && SUPPORTED_BID_TYPES_TEXTUAL.contains(bidTypeTextual) - ? BidType.valueOf(bidTypeTextual) - : null; + private BidType getBidTypeFromExt(ObjectNode bidExt) { + try { + return Optional.ofNullable(bidExt) + .map(ext -> mapper.mapper().convertValue(bidExt, BID_EXT_TYPE_REFERENCE)) + .map(ExtPrebid::getPrebid) + .map(ExtBidPrebid::getMeta) + .map(ExtBidPrebidMeta::getMediaType) + .filter(SUPPORTED_BID_TYPES_TEXTUAL::contains) + .map(BidType::valueOf) + .orElse(null); + } catch (IllegalArgumentException e) { + return null; + } } private static BidType getBidTypeFromImp(String impId, List imps) { diff --git a/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java b/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java index 7cf12c9b1ac..df156830167 100644 --- a/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java +++ b/src/main/java/org/prebid/server/bidder/visx/model/VisxResponse.java @@ -1,13 +1,13 @@ package org.prebid.server.bidder.visx.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class VisxResponse { List seatbid; + + String cur; } diff --git a/src/main/java/org/prebid/server/bidder/visx/model/VisxSeatBid.java b/src/main/java/org/prebid/server/bidder/visx/model/VisxSeatBid.java index 7d7d156d185..3e6eaeeeb3e 100644 --- a/src/main/java/org/prebid/server/bidder/visx/model/VisxSeatBid.java +++ b/src/main/java/org/prebid/server/bidder/visx/model/VisxSeatBid.java @@ -1,12 +1,10 @@ package org.prebid.server.bidder.visx.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class VisxSeatBid { List bid; diff --git a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java index d1973e4465d..aa23176ea97 100644 --- a/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java +++ b/src/main/java/org/prebid/server/bidder/vrtcal/VrtcalBidder.java @@ -94,4 +94,3 @@ private static BidType getBidMediaType(Bid bid) { }; } } - 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..aa16a4367f1 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/vungle/VungleBidder.java @@ -0,0 +1,171 @@ +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 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.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) + .flatMap(seatBid -> seatBid.getBid().stream() + .filter(Objects::nonNull) + .map(bid -> BidderBid.of(bid, BidType.video, seatBid.getSeat(), 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..d3851455d1d 100644 --- a/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.java +++ b/src/main/java/org/prebid/server/bidder/yandex/YandexBidder.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.request.Video; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; @@ -17,8 +18,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; @@ -46,6 +47,8 @@ public class YandexBidder implements Bidder { private static final String PAGE_ID_MACRO = "{{PageId}}"; private static final String IMP_ID_MACRO = "{{ImpId}}"; + private static final String DISPLAY_MANAGER = "prebid.java"; + private static final String DISPLAY_MANAGER_VERSION = "1.1"; private final String endpointUrl; private final JacksonMapper mapper; @@ -86,7 +89,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); } @@ -110,23 +113,31 @@ private ExtImpYandex parseAndValidateImpExt(ObjectNode impExtNode, final String } private static Imp modifyImp(Imp imp) { - if (imp.getBanner() != null) { - return imp.toBuilder().banner(modifyBanner(imp.getBanner())).build(); - } - if (imp.getXNative() != null) { - return imp; + if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) { + throw new PreBidException("Imp #%s must contain at least one valid format (banner, video, or native)" + .formatted(imp.getId())); } - throw new PreBidException("Yandex only supports banner and native types. Ignoring imp id #%s" - .formatted(imp.getId())); + + return imp.toBuilder() + .displaymanager(DISPLAY_MANAGER) + .displaymanagerver(DISPLAY_MANAGER_VERSION) + .banner(modifyBanner(imp.getBanner())) + .video(modifyVideo(imp.getVideo())) + .xNative(imp.getXNative()) + .build(); } private static Banner modifyBanner(Banner banner) { + if (banner == null) { + return null; + } + final Integer weight = banner.getW(); final Integer height = banner.getH(); 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)); @@ -134,6 +145,31 @@ private static Banner modifyBanner(Banner banner) { return banner; } + private static Video modifyVideo(Video video) { + if (video == null) { + return null; + } + + final Integer width = video.getW(); + final Integer height = video.getH(); + if (width == null || height == null || width == 0 || height == 0) { + throw new PreBidException("Invalid sizes provided for Video %sx%s".formatted(width, height)); + } + + final Video.VideoBuilder videoBuilder = video.toBuilder(); + if (video.getMinduration() == null || video.getMinduration() == 0) { + videoBuilder.minduration(1); + } + if (video.getMaxduration() == null || video.getMaxduration() == 0) { + videoBuilder.maxduration(120); + } + if (CollectionUtils.isEmpty(video.getProtocols())) { + videoBuilder.protocols(Collections.singletonList(3)); + } + + return videoBuilder.build(); + } + private String modifyUrl(ExtImpYandex extImpYandex, String referer, String currency) { final String resolvedUrl = endpointUrl .replace(PAGE_ID_MACRO, HttpUtil.encodeUrl(extImpYandex.getPageId().toString())) @@ -167,6 +203,10 @@ private HttpRequest buildHttpRequest(BidRequest outgoingRequest, Str private static MultiMap headers(BidRequest bidRequest) { final MultiMap headers = HttpUtil.headers(); + + headers.add(HttpUtil.X_OPENRTB_VERSION_HEADER, "2.5"); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, getReferer(bidRequest)); + final Device device = bidRequest.getDevice(); if (device != null) { HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.ACCEPT_LANGUAGE_HEADER, device.getLanguage()); @@ -217,18 +257,15 @@ private static BidType getBidType(String bidImpId, List imps) { } private static BidType resolveImpType(Imp imp) { + if (imp.getVideo() != null) { + return BidType.video; + } if (imp.getXNative() != null) { return BidType.xNative; } if (imp.getBanner() != null) { return BidType.banner; } - if (imp.getVideo() != null) { - return BidType.video; - } - if (imp.getAudio() != null) { - return BidType.audio; - } throw new PreBidException("Processing an invalid impression; cannot resolve impression type for imp #%s" .formatted(imp.getId())); } 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..47f6cd44523 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -1,21 +1,28 @@ package org.prebid.server.bidder.yieldlab; -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; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Imp; 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.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; 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.commons.lang3.tuple.Pair; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -23,16 +30,20 @@ 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.YieldlabResponse; +import org.prebid.server.bidder.yieldlab.model.YieldlabBid; 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.DsaTransparency; +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.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.yieldlab.ExtImpYieldlab; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; @@ -40,10 +51,14 @@ 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.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class YieldlabBidder implements Bidder { @@ -52,12 +67,19 @@ public class YieldlabBidder implements Bidder { new TypeReference<>() { }; + private static final TypeReference> YIELDLAB_BID_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BID_CURRENCY = "EUR"; private static final String AD_SLOT_ID_SEPARATOR = ","; private static final String AD_SIZE_SEPARATOR = "x"; 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 @@ -78,11 +100,12 @@ public YieldlabBidder(String endpointUrl, Clock clock, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final ExtImpYieldlab modifiedExtImp = constructExtImp(request.getImp()); + final Map extImps = collectImpExt(request.getImp()); + final ExtImpYieldlab modifiedExtImp = mergeExtImps(extImps.values()); final String uri; try { - uri = makeUrl(modifiedExtImp, request); + uri = makeUrl(modifiedExtImp, request, extImps); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -90,35 +113,32 @@ public Result>> makeHttpRequests(BidRequest request) { return Result.withValue(HttpRequest.builder() .method(HttpMethod.GET) .uri(uri) + .impIds(BidderUtil.impIds(request)) .headers(resolveHeaders(request.getSite(), request.getDevice(), request.getUser())) .build()); } - private ExtImpYieldlab constructExtImp(List imps) { - final List extImps = collectImpExt(imps); - - final List adSlotIds = extImps.stream() + private static ExtImpYieldlab mergeExtImps(Collection extImps) { + final String adSlotIdsParams = extImps.stream() .map(ExtImpYieldlab::getAdslotId) - .filter(Objects::nonNull) - .toList(); + .map(StringUtils::defaultString) + .collect(Collectors.joining(AD_SLOT_ID_SEPARATOR)); - final Map targeting = extImps.stream() + final Map targeting = new HashMap<>(); + extImps.stream() .map(ExtImpYieldlab::getTargeting) .filter(Objects::nonNull) - .flatMap(map -> map.entrySet().stream()) - .filter(entry -> entry.getKey() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (channel1, channel2) -> channel1)); + .forEach(targeting::putAll); - final String adSlotIdsParams = adSlotIds.stream().sorted().collect(Collectors.joining(AD_SLOT_ID_SEPARATOR)); return ExtImpYieldlab.builder().adslotId(adSlotIdsParams).targeting(targeting).build(); } - private List collectImpExt(List imps) { - final List extImps = new ArrayList<>(); + private Map collectImpExt(List imps) { + final Map extImps = new HashMap<>(); for (Imp imp : imps) { final ExtImpYieldlab extImpYieldlab = parseImpExt(imp); if (extImpYieldlab != null) { - extImps.add(extImpYieldlab); + extImps.put(imp.getId(), extImpYieldlab); } } return extImps; @@ -132,10 +152,7 @@ private ExtImpYieldlab parseImpExt(Imp imp) { } } - private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { - // for passing validation tests - final String timestamp = isDebugEnabled(request) ? "200000" : String.valueOf(clock.instant().getEpochSecond()); - + private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request, Map extImps) { final String updatedPath = "%s/%s".formatted(endpointUrl, extImpYieldlab.getAdslotId()); final URIBuilder uriBuilder; @@ -148,12 +165,18 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { uriBuilder .addParameter("content", "json") .addParameter("pvid", "true") - .addParameter("ts", timestamp) + .addParameter("ts", resolveNumberParameter(clock.instant().getEpochSecond())) .addParameter("t", getTargetingValues(extImpYieldlab)); + final String formats = makeFormats(request, extImps); + + 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())); + uriBuilder.addParameter("ids", "ylid:" + StringUtils.defaultString(user.getBuyeruid())); } final Device device = request.getDevice(); @@ -168,8 +191,8 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final Geo geo = device.getGeo(); if (geo != null) { - uriBuilder.addParameter("lat", resolveNumberParameter(geo.getLat())); - uriBuilder.addParameter("lon", resolveNumberParameter(geo.getLon())); + uriBuilder.addParameter("lat", ObjectUtils.defaultIfNull(geo.getLat(), 0f).toString()); + uriBuilder.addParameter("lon", ObjectUtils.defaultIfNull(geo.getLon(), 0f).toString()); } } @@ -186,23 +209,42 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final String consent = getConsentParameter(request.getUser()); if (StringUtils.isNotBlank(consent)) { - uriBuilder.addParameter("consent", consent); + uriBuilder.addParameter("gdpr_consent", consent); } + final String schain = getSchainParameter(request.getSource()); + if (schain != null) { + uriBuilder.addParameter("schain", schain); + } + + extractDsaRequestParamsFromBidRequest(request).forEach(uriBuilder::addParameter); + return uriBuilder.toString(); } - /** - * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. - */ - private static boolean isDebugEnabled(BidRequest bidRequest) { - if (Objects.equals(bidRequest.getTest(), 1)) { - return true; + private String makeFormats(BidRequest request, Map extImps) { + final List formats = new LinkedList<>(); + for (Imp imp: request.getImp()) { + if (!isBanner(imp)) { + continue; + } + final ExtImpYieldlab extImp = extImps.get(imp.getId()); + if (extImp == null) { + continue; + } + + final String formatsPerAdSlotString = CollectionUtils.emptyIfNull(imp.getBanner().getFormat()).stream() + .map(format -> "%dx%d".formatted(format.getW(), format.getH())) + .collect(Collectors.joining("|")); + + formats.add("%s:%s".formatted(extImp.getAdslotId(), formatsPerAdSlotString)); } - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; - return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); + 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; } private String getTargetingValues(ExtImpYieldlab extImpYieldlab) { @@ -216,196 +258,319 @@ private String getTargetingValues(ExtImpYieldlab extImpYieldlab) { } private static String getGdprParameter(Regs regs) { - if (regs != null) { - final Integer gdpr = regs.getExt() != null ? regs.getExt().getGdpr() : null; - if (gdpr != null && (gdpr == 0 || gdpr == 1)) { - return gdpr.toString(); - } - } - return ""; + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .filter(gdpr -> gdpr == 0 || gdpr == 1) + .map(Object::toString) + .orElse(StringUtils.EMPTY); } private static String getConsentParameter(User user) { - final ExtUser extUser = user != null ? user.getExt() : null; - final String consent = extUser != null ? extUser.getConsent() : null; - return ObjectUtils.defaultIfNull(consent, ""); + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(StringUtils.EMPTY); } - private static MultiMap resolveHeaders(Site site, Device device, User user) { - final MultiMap headers = MultiMap.caseInsensitiveMultiMap() - .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); + private String getSchainParameter(Source source) { + return Optional.ofNullable(source) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .orElse(null); + } - if (site != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER.toString(), site.getPage()); + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes)) { + return null; } - if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER.toString(), device.getIp()); - } + final StringBuilder schainBuilder = new StringBuilder(); - if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - headers.add(HttpUtil.COOKIE_HEADER.toString(), "id=" + user.getBuyeruid()); + schainBuilder.append(schain.getVer()); + schainBuilder.append(","); + schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); + for (SupplyChainNode node : schain.getNodes()) { + schainBuilder.append("!"); + schainBuilder.append(encodeValue(node.getAsi())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getSid())); + schainBuilder.append(","); + + schainBuilder.append(node.getHp() == null ? StringUtils.EMPTY : node.getHp()); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getRid())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getName())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getDomain())); + schainBuilder.append(","); + + schainBuilder.append(node.getExt() == null + ? StringUtils.EMPTY + : HttpUtil.encodeUrl(mapper.encodeToString(node.getExt()))); } - return headers; + return schainBuilder.toString(); } - @Override - public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final List yieldlabResponses; - try { - yieldlabResponses = decodeBodyToBidList(httpCall); - } catch (PreBidException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + private static String encodeValue(String value) { + return value == null ? StringUtils.EMPTY : HttpUtil.encodeUrl(value); + } + + 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()); } - final List bidderBids = new ArrayList<>(); - for (int i = 0; i < yieldlabResponses.size(); i++) { - final BidderBid bidderBid; - try { - bidderBid = resolveBidderBid(yieldlabResponses, i, bidRequest); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } + if (dsa.getPubRender() != null) { + dsaRequestParams.put("dsapubrender", dsa.getPubRender().toString()); + } + + if (dsa.getDataToPub() != null) { + dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString()); + } - if (bidderBid != null) { - bidderBids.add(bidderBid); + final List dsaTransparency = dsa.getTransparency(); + if (CollectionUtils.isNotEmpty(dsaTransparency)) { + final String encodedTransparencies = encodeTransparenciesAsString(dsaTransparency); + if (StringUtils.isNotBlank(encodedTransparencies)) { + dsaRequestParams.put("dsatransparency", encodedTransparencies); } } - return Result.of(bidderBids, Collections.emptyList()); + + return dsaRequestParams; } - private BidderBid resolveBidderBid(List yieldlabResponses, - int currentImpIndex, BidRequest bidRequest) { - final YieldlabResponse yieldlabResponse = yieldlabResponses.get(currentImpIndex); + private static String encodeTransparenciesAsString(List transparencies) { + return transparencies.stream() + .map(YieldlabBidder::encodeTransparency) + .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER)); + } - final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); - if (matchedExtImp == null) { - throw new PreBidException("Invalid extension"); + private static String encodeTransparency(DsaTransparency transparency) { + final String domain = transparency.getDomain(); + if (StringUtils.isBlank(domain)) { + return StringUtils.EMPTY; } - final Imp currentImp = bidRequest.getImp().get(currentImpIndex); - if (currentImp == null) { - throw new PreBidException("Imp not present for id " + currentImpIndex); + final List dsaParams = transparency.getDsaParams(); + if (CollectionUtils.isEmpty(dsaParams)) { + return domain; } - final Bid.BidBuilder updatedBid = Bid.builder(); - final BidType bidType; - if (currentImp.getVideo() != null) { - bidType = BidType.video; - updatedBid.nurl(makeNurl(bidRequest, matchedExtImp, yieldlabResponse)); - updatedBid.adm(resolveAdm(bidRequest, matchedExtImp, yieldlabResponse)); - } else if (currentImp.getBanner() != null) { - bidType = BidType.banner; - updatedBid.adm(makeAdm(bidRequest, matchedExtImp, yieldlabResponse)); - } else { - return null; + return TRANSPARENCY_TEMPLATE.formatted(domain, encodeTransparencyParams(dsaParams)); + } + + private static String encodeTransparencyParams(List dsaParams) { + return dsaParams.stream() + .map(param -> ObjectUtils.defaultIfNull(param, 0)) + .map(Object::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); + + if (site != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); } - addBidParams(yieldlabResponse, bidRequest, updatedBid) - .impid(currentImp.getId()); + if (device != null) { + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); + } - return BidderBid.of(updatedBid.build(), bidType, BID_CURRENCY); + final String buyerUid = user != null ? user.getBuyeruid() : null; + if (StringUtils.isNotBlank(buyerUid)) { + headers.add(HttpUtil.COOKIE_HEADER, "id=" + buyerUid); + } + + return headers; } - private List decodeBodyToBidList(BidderCall httpCall) { + @Override + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List errors = new ArrayList<>(); try { - return mapper.mapper().readValue( + final List yieldlabBids = mapper.decodeValue( httpCall.getResponse().getBody(), - mapper.mapper().getTypeFactory().constructCollectionType(List.class, YieldlabResponse.class)); - } catch (DecodeException | JsonProcessingException e) { - throw new PreBidException(e.getMessage()); + YIELDLAB_BID_TYPE_REFERENCE); + return Result.of(extractBids(bidRequest, yieldlabBids, errors), errors); + } catch (DecodeException | PreBidException e) { + return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private ExtImpYieldlab getMatchedExtImp(Integer responseId, List imps) { - return collectImpExt(imps).stream() - .filter(ext -> ext.getAdslotId().equals(String.valueOf(responseId))) - .findFirst() - .orElse(null); + private List extractBids(BidRequest bidRequest, + List yieldlabBids, + List errors) { + + if (CollectionUtils.isEmpty(yieldlabBids)) { + return Collections.emptyList(); + } + + final Map> adSlotMap = new HashMap<>(); + for (Imp imp : bidRequest.getImp()) { + final ExtImpYieldlab extImpYieldlab = parseImpExt(imp); + if (extImpYieldlab != null) { + adSlotMap.put(extImpYieldlab.getAdslotId(), Pair.of(imp, extImpYieldlab)); + } + } + + return yieldlabBids.stream() + .filter(Objects::nonNull) + .map(bid -> makeBid(bidRequest, bid, adSlotMap, errors)) + .filter(Objects::nonNull) + .toList(); } - private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidRequest bidRequest, - Bid.BidBuilder updatedBid) { - final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); + private BidderBid makeBid(BidRequest bidRequest, + YieldlabBid yieldlabBid, + Map> adSlotMap, + List errors) { + + final String adSlotId = resolveNumberParameter(yieldlabBid.getId()); + final Pair impPair = adSlotMap.get(adSlotId); - if (matchedExtImp == null) { - throw new PreBidException("Invalid extension"); + if (impPair == null) { + throw new PreBidException(("failed to find yieldlab request for adslotID %d. " + + "This is most likely a programming issue").formatted(yieldlabBid.getId())); } - updatedBid.id(resolveNumberParameter(yieldlabResponse.getId())) - .price(resolvePrice(yieldlabResponse.getPrice())) - .dealid(resolveNumberParameter(yieldlabResponse.getPid())) - .crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp)) - .w(resolveSizeParameter(yieldlabResponse.getAdSize(), true)) - .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)); + final Imp imp = impPair.getKey(); + final ExtImpYieldlab extImp = impPair.getValue(); + final BidType bidType = resolveBidType(imp); - return updatedBid; - } + if (bidType == null) { + return null; + } - private static BigDecimal resolvePrice(Double price) { - return price != null ? BigDecimal.valueOf(price / 100) : null; + final Format adsize = resolveAdSize(yieldlabBid.getAdSize()); + final String advertiser = yieldlabBid.getAdvertiser(); + final Bid bid = Bid.builder() + .id(adSlotId) + .price(BigDecimal.valueOf(yieldlabBid.getPrice() / 100)) + .impid(imp.getId()) + .crid(makeCreativeId(yieldlabBid, adSlotId)) + .dealid(resolveNumberParameter(yieldlabBid.getPid())) + .nurl(bidType == BidType.video ? makeNurl(bidRequest, extImp, yieldlabBid) : null) + .adm(bidType == BidType.video + ? makeVast(bidRequest, extImp, yieldlabBid) + : makeBanner(bidRequest, extImp, yieldlabBid)) + .w(adsize.getW()) + .h(adsize.getH()) + .adomain(advertiser != null ? Collections.singletonList(advertiser) : null) + .ext(resolveBidExt(yieldlabBid, errors)) + .build(); + + return BidderBid.of(bid, bidType, BID_CURRENCY); } - private static String resolveNumberParameter(Number param) { - return param != null ? String.valueOf(param) : null; + private static BidType resolveBidType(Imp imp) { + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getBanner() != null) { + return BidType.banner; + } else { + return null; + } } - private static String makeCreativeId(BidRequest bidRequest, YieldlabResponse yieldlabResponse, - ExtImpYieldlab extImp) { - // for passing validation tests - final int weekNumber = isDebugEnabled(bidRequest) ? 35 : Calendar.getInstance().get(Calendar.WEEK_OF_YEAR); - return CREATIVE_ID.formatted(extImp.getAdslotId(), yieldlabResponse.getPid(), weekNumber); - } + private static Format resolveAdSize(String adsize) { + if (adsize == null) { + return Format.builder().w(0).h(0).build(); + } - private static Integer resolveSizeParameter(String adSize, boolean isWidth) { - final String[] sizeParts = adSize.split(AD_SIZE_SEPARATOR); + final String[] sizes = adsize.split(AD_SIZE_SEPARATOR); + if (sizes.length != 2) { + return Format.builder().w(0).h(0).build(); + } - if (sizeParts.length != 2) { - return 0; + try { + return Format.builder() + .w(Integer.parseUnsignedInt(sizes[0], 10)) + .h(Integer.parseUnsignedInt(sizes[1], 10)) + .build(); + } catch (NumberFormatException e) { + throw new PreBidException("failed to parse yieldlab adsize"); } - final int sizeIndex = isWidth ? 0 : 1; - return StringUtils.isNumeric(sizeParts[sizeIndex]) ? Integer.parseInt(sizeParts[sizeIndex]) : 0; } - private String makeAdm(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - return AD_SOURCE_BANNER.formatted(makeNurl(bidRequest, extImpYieldlab, yieldlabResponse)); + private static String makeCreativeId(YieldlabBid yieldlabBid, String adSlotId) { + return CREATIVE_ID.formatted(adSlotId, yieldlabBid.getPid(), Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)); } - private String resolveAdm(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - return VAST_MARKUP.formatted( - extImpYieldlab.getAdslotId(), - makeNurl(bidRequest, extImpYieldlab, yieldlabResponse)); + private String makeBanner(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { + return AD_SOURCE_BANNER.formatted(makeNurl(bidRequest, extImp, yieldlabBid)); } - private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - // for passing validation tests - final String timestamp = isDebugEnabled(bidRequest) - ? "200000" - : String.valueOf(clock.instant().getEpochSecond()); + private String makeVast(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { + return VAST_MARKUP.formatted(extImp.getAdslotId(), makeNurl(bidRequest, extImp, yieldlabBid)); + } + private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { final URIBuilder uriBuilder = new URIBuilder() - .addParameter("ts", timestamp) - .addParameter("id", extImpYieldlab.getExtId()) - .addParameter("pvid", yieldlabResponse.getPvid()); + .addParameter("ts", resolveNumberParameter(clock.instant().getEpochSecond())) + .addParameter("id", extImp.getExtId()) + .addParameter("pvid", yieldlabBid.getPvid()); final User user = bidRequest.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); + uriBuilder.addParameter("ids", "ylid:" + StringUtils.defaultString(user.getBuyeruid())); } final String gdpr = getGdprParameter(bidRequest.getRegs()); final String consent = getConsentParameter(bidRequest.getUser()); if (StringUtils.isNotBlank(gdpr) && StringUtils.isNotBlank(consent)) { - uriBuilder.addParameter("gdpr", gdpr) - .addParameter("consent", consent); + uriBuilder + .addParameter("gdpr", gdpr) + .addParameter("gdpr_consent", consent); } return AD_SOURCE_URL.formatted( - extImpYieldlab.getAdslotId(), - extImpYieldlab.getSupplyId(), - yieldlabResponse.getAdSize(), + extImp.getAdslotId(), + extImp.getSupplyId(), + yieldlabBid.getAdSize(), uriBuilder.toString().replace("?", "")); } + + private ObjectNode resolveBidExt(YieldlabBid bid, List errors) { + final ExtBidDsa dsa = bid.getDsa(); + if (dsa == null) { + return null; + } + final ObjectNode ext = mapper.mapper().createObjectNode(); + final JsonNode dsaNode; + try { + dsaNode = mapper.mapper().valueToTree(dsa); + } catch (IllegalArgumentException e) { + errors.add(BidderError.badServerResponse( + "Failed to serialize DSA object for adslot %d".formatted(bid.getId()))); + return null; + } + ext.set("dsa", dsaNode); + return ext; + } + + private static String resolveNumberParameter(Number param) { + return param != null ? String.valueOf(param) : null; + } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java new file mode 100644 index 00000000000..a864733f232 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.yieldlab.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; + +@Value(staticConstructor = "of") +public class YieldlabBid { + + Long id; + + Double price; + + String advertiser; + + @JsonProperty("adsize") + String adSize; + + Long pid; + + Long did; + + String pvid; + + ExtBidDsa dsa; +} 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 deleted file mode 100644 index 4cd1dbf7294..00000000000 --- a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.bidder.yieldlab.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class YieldlabResponse { - - Integer id; - - Double price; - - String advertiser; - - @JsonProperty("adsize") - String adSize; - - Integer pid; - - Integer did; - - String pvid; -} 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..a927bb15f56 100644 --- a/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldmo/YieldmoBidder.java @@ -15,9 +15,11 @@ 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.yieldmo.proto.YieldmoBidExt; import org.prebid.server.bidder.yieldmo.proto.YieldmoImpExt; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; @@ -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, bidRequest) : bidFloorPrice; + + return imp.toBuilder() + .bidfloor(bidFloorPrice.getValue()) + .bidfloorcur(bidFloorPrice.getCurrency()) + .ext(mapper.mapper().valueToTree(modifiedExt)) + .build(); + } + + private Price convertBidFloor(Price bidFloorPrice, 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/yieldmo/proto/YieldmoBidExt.java b/src/main/java/org/prebid/server/bidder/yieldmo/proto/YieldmoBidExt.java index 54742a92d24..ca7365c5388 100644 --- a/src/main/java/org/prebid/server/bidder/yieldmo/proto/YieldmoBidExt.java +++ b/src/main/java/org/prebid/server/bidder/yieldmo/proto/YieldmoBidExt.java @@ -12,4 +12,3 @@ public class YieldmoBidExt { @JsonProperty("mediatype") String mediaType; } - 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/zentotem/ZentotemBidder.java b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java new file mode 100644 index 00000000000..1aac754f04a --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/zentotem/ZentotemBidder.java @@ -0,0 +1,107 @@ +package org.prebid.server.bidder.zentotem; + +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; +import java.util.stream.Collectors; + +public class ZentotemBidder implements Bidder { + + private final String endpointUrl; + private final JacksonMapper mapper; + + public ZentotemBidder(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()) { + try { + final BidRequest outgoingRequest = request.toBuilder() + .imp(Collections.singletonList(imp)) + .build(); + httpRequests.add(BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper)); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + } + } + + return Result.of(httpRequests, errors); + } + + @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 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) + .filter(Objects::nonNull) + .map(bid -> makeBidderBid(bid, bidResponse.getCur(), errors)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static BidderBid makeBidderBid(Bid bid, String currency, List errors) { + final BidType bidType = getBidType(bid, errors); + return bidType != null + ? BidderBid.of(bid, bidType, currency) + : null; + } + + private static BidType getBidType(Bid bid, List errors) { + return switch (bid.getMtype()) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + case 4 -> BidType.xNative; + case null, default -> { + errors.add(BidderError.badServerResponse( + "could not define media type for impression: " + bid.getImpid())); + yield null; + } + }; + } +} 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..a665f00c845 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/BasicPbcStorageService.java @@ -0,0 +1,235 @@ +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.metric.MetricName; +import org.prebid.server.metric.Metrics; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.HttpClient; + +import java.net.URL; +import java.time.Clock; +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; + private final Clock clock; + private final Metrics metrics; + + public BasicPbcStorageService(HttpClient httpClient, + URL endpointUrl, + String apiKey, + int callTimeoutMs, + JacksonMapper mapper, + Clock clock, + Metrics metrics) { + + this.httpClient = Objects.requireNonNull(httpClient); + this.endpointUrl = Objects.requireNonNull(endpointUrl); + this.apiKey = Objects.requireNonNull(apiKey); + this.callTimeoutMs = callTimeoutMs; + this.mapper = Objects.requireNonNull(mapper); + this.clock = Objects.requireNonNull(clock); + this.metrics = Objects.requireNonNull(metrics); + } + + @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 String valueToStore = prepareValueForStoring(value, type); + final ModuleCacheRequest moduleCacheRequest = + ModuleCacheRequest.of( + constructEntryKey(key, appCode), + type, + valueToStore, + application, + ttlseconds); + + updateCreativeMetrics(valueToStore, type, ttlseconds, appCode); + + final long startTime = clock.millis(); + return httpClient.post( + endpointUrl.toString(), + securedCallHeaders(), + mapper.encodeToString(moduleCacheRequest), + callTimeoutMs) + .compose(response -> processStoreResponse( + response.getStatusCode(), + response.getBody(), + startTime, + appCode)); + + } + + 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 void updateCreativeMetrics(String value, StorageDataType type, Integer ttlseconds, String appCode) { + final MetricName metricName = switch (type) { + case XML -> MetricName.xml; + case JSON -> MetricName.json; + case TEXT -> MetricName.text; + }; + + metrics.updateModuleStorageCacheEntrySize(appCode, value.length(), metricName); + if (ttlseconds != null) { + metrics.updateModuleStorageCacheEntryTtl(appCode, ttlseconds, metricName); + } + } + + 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, long startTime, String appCode) { + if (statusCode != 204) { + metrics.updateModuleStorageCacheWriteRequestTime(appCode, clock.millis() - startTime, MetricName.err); + throw new PreBidException("HTTP status code: '%s', body: '%s' " + .formatted(statusCode, responseBody)); + } + + metrics.updateModuleStorageCacheWriteRequestTime(appCode, clock.millis() - startTime, MetricName.ok); + 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); + } + + final long startTime = clock.millis(); + return httpClient.get( + getRetrieveEndpoint(key, appCode, application), + securedCallHeaders(), + callTimeoutMs) + .map(response -> toModuleCacheResponse( + response.getStatusCode(), + response.getBody(), + startTime, + appCode)); + + } + + 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, + long startTime, + String application) { + + if (statusCode != 200) { + metrics.updateModuleStorageCacheReadRequestTime(application, clock.millis() - startTime, MetricName.err); + throw new PreBidException("HTTP status code " + statusCode); + } + + metrics.updateModuleStorageCacheReadRequestTime(application, clock.millis() - startTime, MetricName.ok); + + 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..6b028eb9ae8 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java @@ -0,0 +1,717 @@ +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.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +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.CacheErrorResponse; +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.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.URISyntaxException; +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.BiConsumer; +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 static final String TRACE_INFO_SEPARATOR = "-"; + private static final int MAX_DATACENTER_REGION_LENGTH = 4; + private static final String UUID_QUERY_PARAMETER = "uuid"; + private static final String CH_QUERY_PARAMETER = "ch"; + + private final HttpClient httpClient; + private final URL externalEndpointUrl; + private final URL internalEndpointUrl; + 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; + + private final boolean appendTraceInfoToCacheId; + private final String datacenterRegion; + + public CoreCacheService( + HttpClient httpClient, + URL externalEndpointUrl, + URL internalEndpointUrl, + String cachedAssetUrlTemplate, + long expectedCacheTimeMs, + String apiKey, + boolean isApiKeySecured, + boolean appendTraceInfoToCacheId, + String datacenterRegion, + VastModifier vastModifier, + EventsService eventsService, + Metrics metrics, + Clock clock, + UUIDIdGenerator idGenerator, + JacksonMapper mapper) { + + this.httpClient = Objects.requireNonNull(httpClient); + this.externalEndpointUrl = Objects.requireNonNull(externalEndpointUrl); + this.internalEndpointUrl = internalEndpointUrl; + 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); + + this.appendTraceInfoToCacheId = appendTraceInfoToCacheId; + this.datacenterRegion = normalizeDatacenterRegion(datacenterRegion); + } + + public String getEndpointHost() { + final String host = externalEndpointUrl.getHost(); + final int port = externalEndpointUrl.getPort(); + return port != -1 ? "%s:%d".formatted(host, port) : host; + } + + public String getEndpointPath() { + return externalEndpointUrl.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( + ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).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( + ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).toString(), + cacheHeaders, + mapper.encodeToString(bidCacheRequest), + remainingTimeout) + .map(response -> processVtrackWriteCacheResponse( + response.getStatusCode(), response.getBody(), bidCount, accountId, startTime)) + .recover(exception -> failVtrackCacheWriteResponse(exception, accountId, startTime)); + } + + private BidCacheResponse processVtrackWriteCacheResponse(int statusCode, + String responseBody, + int bidCount, + String accountId, + long startTime) { + + final BidCacheResponse bidCacheResponse = toBidCacheResponse(statusCode, responseBody, bidCount); + metrics.updateVtrackCacheWriteRequestTime(accountId, clock.millis() - startTime, MetricName.ok); + return bidCacheResponse; + } + + public Future cachePutObjects(List bidPutObjects, + Boolean isEventsEnabled, + Set biddersAllowingVastUpdate, + String accountId, + Integer accountTtl, + String integration, + Timeout timeout) { + + final List cachedCreatives = updatePutObjects( + bidPutObjects, isEventsEnabled, biddersAllowingVastUpdate, accountId, accountTtl, integration); + + updateCreativeMetrics( + cachedCreatives, + (ttl, type) -> metrics.updateVtrackCacheCreativeTtl(accountId, ttl, type), + (size, type) -> metrics.updateVtrackCacheCreativeSize(accountId, size, type)); + + return makeRequest(toBidCacheRequest(cachedCreatives), cachedCreatives.size(), timeout, accountId); + } + + private List updatePutObjects(List bidPutObjects, + Boolean isEventsEnabled, + Set allowedBidders, + String accountId, + Integer accountTtl, + String integration) { + + return bidPutObjects.stream() + .map(putObject -> putObject.toBuilder() + // remove "/vtrack" specific fields + .bidid(null) + .bidder(null) + .timestamp(null) + .key(resolveCacheKey(accountId, putObject.getKey())) + .value(vastModifier.modifyVastXml(isEventsEnabled, + allowedBidders, + putObject, + accountId, + integration)) + .ttlseconds(resolveVtrackTtl(putObject.getTtlseconds(), accountTtl)) + .build()) + .map(payload -> CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue()))) + .toList(); + } + + private static Integer resolveVtrackTtl(Integer initialObjectTtl, Integer initialAccountTtl) { + final Integer accountTtl = initialAccountTtl != null && initialAccountTtl > 0 ? initialAccountTtl : null; + final Integer objectTtl = initialObjectTtl != null && initialObjectTtl > 0 ? initialObjectTtl : null; + return ObjectUtils.min(objectTtl, accountTtl); + } + + 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.getVastTtl())) + .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, accountId))) + .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( + cachedCreatives, + (ttl, type) -> metrics.updateCacheCreativeTtl(accountId, ttl, type), + (size, type) -> metrics.updateCacheCreativeSize(accountId, size, type)); + + final String url = ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl).toString(); + final String body = mapper.encodeToString(bidCacheRequest); + final CacheHttpRequest httpRequest = CacheHttpRequest.of(externalEndpointUrl.toString(), 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( + externalEndpointUrl.toString(), httpRequest, httpResponse, startTime); + final BidCacheResponse bidCacheResponse; + try { + bidCacheResponse = toBidCacheResponse(responseStatusCode, response.getBody(), bidCount); + metrics.updateAuctionCacheRequestTime(accountId, clock.millis() - startTime, MetricName.ok); + } 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.updateAuctionCacheRequestTime(accountId, clock.millis() - startTime, MetricName.err); + + final DebugHttpCall httpCall = makeDebugHttpCall(externalEndpointUrl.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 String resolvedCacheKey = resolveCacheKey(accountId); + + final BidPutObject payload = BidPutObject.builder() + .aid(eventsContext.getAuctionId()) + .type("json") + .key(resolvedCacheKey) + .value(bidObjectNode) + .ttlseconds(cacheBid.getTtl()) + .build(); + + return CachedCreative.of(payload, creativeSizeFromAdm(bid.getAdm())); + } + + private CachedCreative createXmlPutObjectOpenrtb(CacheBid cacheBid, + String requestId, + String hbCacheId, + String accountId) { + + final BidInfo bidInfo = cacheBid.getBidInfo(); + final Bid bid = bidInfo.getBid(); + final String vastXml = bid.getAdm(); + + final BidPutObject payload = BidPutObject.builder() + .aid(requestId) + .type("xml") + .key(resolveCacheKey(accountId, hbCacheId, bidInfo.getCategory())) + .value(vastXml != null ? new TextNode(vastXml) : null) + .ttlseconds(cacheBid.getTtl()) + .build(); + + return CachedCreative.of(payload, creativeSizeFromTextNode(payload.getValue())); + } + + private static String formatCategoryMappedCacheKey(String hbCacheId, String category) { + return StringUtils.isNoneEmpty(category, hbCacheId) + ? "%s_%s".formatted(category, hbCacheId) + : hbCacheId; + } + + 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) { + + 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"); + } + + 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(List cachedCreatives, + BiConsumer updateCreativeTtlMetric, + BiConsumer updateCreativeSiseMetric) { + + for (CachedCreative cachedCreative : cachedCreatives) { + final BidPutObject payload = cachedCreative.getPayload(); + final MetricName creativeType = resolveCreativeTypeName(payload); + final Integer creativeTtl = ObjectUtils.defaultIfNull(payload.getTtlseconds(), payload.getExpiry()); + + if (creativeTtl != null) { + updateCreativeTtlMetric.accept(creativeTtl, creativeType); + } + + updateCreativeSiseMetric.accept(cachedCreative.getSize(), creativeType); + } + } + + 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()); + } + + private String resolveCacheKey(String accountId, String existingKey, String category) { + final String resolvedCacheKey = resolveCacheKey(accountId, existingKey); + return formatCategoryMappedCacheKey(resolvedCacheKey, category); + + } + + private String resolveCacheKey(String accountId) { + return resolveCacheKey(accountId, null); + } + + private String resolveCacheKey(String accountId, String existingCacheKey) { + if (!appendTraceInfoToCacheId || existingCacheKey != null) { + return existingCacheKey; + } + + final boolean isDatacenterNamePopulated = StringUtils.isNotBlank(datacenterRegion); + final int separatorCount = isDatacenterNamePopulated ? 2 : 1; + final int accountIdLength = accountId.length(); + final int traceInfoLength = isDatacenterNamePopulated + ? accountIdLength + datacenterRegion.length() + separatorCount + : accountIdLength + separatorCount; + + final String cacheKey = idGenerator.generateId(); + if (cacheKey == null || traceInfoLength >= (cacheKey.length() / 2)) { + return null; + } + + final String substring = cacheKey.substring(0, cacheKey.length() - traceInfoLength); + return isDatacenterNamePopulated + ? accountId + TRACE_INFO_SEPARATOR + datacenterRegion + TRACE_INFO_SEPARATOR + substring + : accountId + TRACE_INFO_SEPARATOR + substring; + } + + private static String normalizeDatacenterRegion(String datacenterRegion) { + if (datacenterRegion == null) { + return null; + } + + final String trimmedDatacenterRegion = datacenterRegion.trim(); + return trimmedDatacenterRegion.length() > MAX_DATACENTER_REGION_LENGTH + ? trimmedDatacenterRegion.substring(0, MAX_DATACENTER_REGION_LENGTH) + : trimmedDatacenterRegion; + } + + public Future getCachedObject(String key, String ch, Timeout timeout) { + final long remainingTimeout = timeout.remaining(); + if (remainingTimeout <= 0) { + return Future.failedFuture(new TimeoutException("Timeout has been exceeded")); + } + + final URL endpointUrl = ObjectUtils.firstNonNull(internalEndpointUrl, externalEndpointUrl); + final String url; + try { + final URIBuilder uriBuilder = new URIBuilder(endpointUrl.toString()); + uriBuilder.addParameter(UUID_QUERY_PARAMETER, key); + if (StringUtils.isNotBlank(ch)) { + uriBuilder.addParameter(CH_QUERY_PARAMETER, ch); + } + url = uriBuilder.build().toString(); + } catch (URISyntaxException e) { + return Future.failedFuture(new IllegalArgumentException("Configured cache url is malformed", e)); + } + + final long startTime = clock.millis(); + return httpClient.get(url, cacheHeaders, remainingTimeout) + .map(response -> processVtrackReadResponse(response, startTime)) + .recover(exception -> failVtrackCacheReadResponse(exception, startTime)); + } + + private HttpClientResponse processVtrackReadResponse(HttpClientResponse response, long startTime) { + final int statusCode = response.getStatusCode(); + final String body = response.getBody(); + + if (statusCode == 200) { + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.ok); + return response; + } + + try { + final CacheErrorResponse errorResponse = mapper.decodeValue(body, CacheErrorResponse.class); + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.err); + return HttpClientResponse.of(statusCode, response.getHeaders(), errorResponse.getMessage()); + } catch (DecodeException e) { + throw new PreBidException("Cannot parse response: " + body, e); + } + } + + private Future failVtrackCacheWriteResponse(Throwable exception, String accountId, long startTime) { + if (exception instanceof PreBidException) { + metrics.updateVtrackCacheWriteRequestTime(accountId, clock.millis() - startTime, MetricName.err); + } + return failResponse(exception); + } + + private Future failVtrackCacheReadResponse(Throwable exception, long startTime) { + if (exception instanceof PreBidException) { + metrics.updateVtrackCacheReadRequestTime(clock.millis() - startTime, MetricName.err); + } + return failResponse(exception); + } + + private static Future failResponse(Throwable exception) { + 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); + } +} 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..d6e2414e65d 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheBid.java +++ b/src/main/java/org/prebid/server/cache/model/CacheBid.java @@ -1,16 +1,14 @@ package org.prebid.server.cache.model; import com.iab.openrtb.response.Bid; -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 sent to {@link CoreCacheService}. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheBid { BidInfo bidInfo; 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/CacheHttpRequest.java b/src/main/java/org/prebid/server/cache/model/CacheHttpRequest.java index 36d2127810d..aad8728949e 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheHttpRequest.java +++ b/src/main/java/org/prebid/server/cache/model/CacheHttpRequest.java @@ -1,13 +1,11 @@ package org.prebid.server.cache.model; -import lombok.AllArgsConstructor; import lombok.Value; /** * Holds HTTP request info. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheHttpRequest { String uri; diff --git a/src/main/java/org/prebid/server/cache/model/CacheHttpResponse.java b/src/main/java/org/prebid/server/cache/model/CacheHttpResponse.java index cb260d31e79..60ccea28fdb 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheHttpResponse.java +++ b/src/main/java/org/prebid/server/cache/model/CacheHttpResponse.java @@ -1,14 +1,11 @@ package org.prebid.server.cache.model; -import lombok.AllArgsConstructor; import lombok.Value; /** * Holds HTTP response info. */ - -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheHttpResponse { int statusCode; diff --git a/src/main/java/org/prebid/server/cache/model/CacheInfo.java b/src/main/java/org/prebid/server/cache/model/CacheInfo.java index 5bfefcc1f97..13623b91208 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheInfo.java +++ b/src/main/java/org/prebid/server/cache/model/CacheInfo.java @@ -1,13 +1,11 @@ package org.prebid.server.cache.model; -import lombok.AllArgsConstructor; import lombok.Value; /** * Used to determine cache IDs targeting keywords should be in response */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheInfo { private static final CacheInfo EMPTY = CacheInfo.of(null, null, null, null); diff --git a/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java b/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java index e40d088faa0..4ffd3799692 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java +++ b/src/main/java/org/prebid/server/cache/model/CacheServiceResult.java @@ -1,7 +1,6 @@ package org.prebid.server.cache.model; import com.iab.openrtb.response.Bid; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Collections; @@ -10,8 +9,7 @@ /** * Holds the result of bids caching. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheServiceResult { private static final CacheServiceResult EMPTY = CacheServiceResult.of(null, null, Collections.emptyMap()); diff --git a/src/main/java/org/prebid/server/cache/model/CacheTtl.java b/src/main/java/org/prebid/server/cache/model/CacheTtl.java index e3f28c68f80..ee4df9f7cea 100644 --- a/src/main/java/org/prebid/server/cache/model/CacheTtl.java +++ b/src/main/java/org/prebid/server/cache/model/CacheTtl.java @@ -1,6 +1,5 @@ package org.prebid.server.cache.model; -import lombok.AllArgsConstructor; import lombok.Value; /** @@ -8,17 +7,10 @@ *

* Used for representing configuration. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheTtl { - private static final CacheTtl EMPTY = CacheTtl.of(null, null); - Integer bannerCacheTtl; Integer videoCacheTtl; - - public static CacheTtl empty() { - return EMPTY; - } } 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/model/DebugHttpCall.java b/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java index f995fa19ca9..19f0a3440b4 100644 --- a/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java +++ b/src/main/java/org/prebid/server/cache/model/DebugHttpCall.java @@ -13,8 +13,6 @@ @Builder public class DebugHttpCall { - private static final DebugHttpCall EMPTY = DebugHttpCall.builder().build(); - String endpoint; String requestUri; @@ -28,8 +26,4 @@ public class DebugHttpCall { Map> requestHeaders; Integer responseTimeMillis; - - public static DebugHttpCall empty() { - return EMPTY; - } } 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/CacheErrorResponse.java b/src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java new file mode 100644 index 00000000000..cf535ac1181 --- /dev/null +++ b/src/main/java/org/prebid/server/cache/proto/response/CacheErrorResponse.java @@ -0,0 +1,19 @@ +package org.prebid.server.cache.proto.response; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class CacheErrorResponse { + + String error; + + Integer status; + + String path; + + String message; + + Long timestamp; +} 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..4ebd38c45ed 100644 --- a/src/main/java/org/prebid/server/cookie/CookieSyncService.java +++ b/src/main/java/org/prebid/server/cookie/CookieSyncService.java @@ -43,6 +43,7 @@ import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.StreamUtil; import java.util.ArrayList; import java.util.Collection; @@ -54,7 +55,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -111,6 +111,8 @@ public Future processContext(CookieSyncContext cookieSyncCont .map(this::filterDisabledBidders) .map(this::filterBiddersWithoutUsersync) .map(this::filterBiddersWithDisabledUsersync) + .map(this::filterBiddersByGdpr) + .map(this::filterBiddersByGppSid) .map(this::applyRequestFilterSettings) .compose(this::applyPrivacyFilteringRules) .map(this::filterInSyncBidders); @@ -202,6 +204,26 @@ private CookieSyncContext filterBiddersWithDisabledUsersync(CookieSyncContext co RejectionReason.DISABLED_USERSYNC); } + private CookieSyncContext filterBiddersByGdpr(CookieSyncContext cookieSyncContext) { + return filterBidders( + cookieSyncContext, + bidder -> cookieSyncContext.getPrivacyContext().getTcfContext().isInGdprScope() + && bidderCatalog.usersyncerByName(bidder).map(Usersyncer::isSkipWhenInGdprScope).orElse(false), + RejectionReason.REJECTED_BY_REGULATION_SCOPE); + } + + private CookieSyncContext filterBiddersByGppSid(CookieSyncContext cookieSyncContext) { + return filterBidders( + cookieSyncContext, + bidder -> bidderCatalog.usersyncerByName(bidder) + .map(Usersyncer::getGppSidToSkip) + .map(gppSid -> !Collections.disjoint( + gppSid, + cookieSyncContext.getCookieSyncRequest().getGppSid())) + .orElse(false), + RejectionReason.REJECTED_BY_REGULATION_SCOPE); + } + /** * should be called after applying request filter, as it will populate usersync data */ @@ -385,16 +407,11 @@ private static Set allowedBiddersByPriority(CookieSyncContext cookieSync private List validStatuses(Set biddersToSync, CookieSyncContext cookieSyncContext) { return biddersToSync.stream() - .filter(distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow())) + .filter(StreamUtil.distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow())) .map(bidder -> validStatus(bidder, cookieSyncContext)) .toList(); } - private static Predicate distinctBy(Function keyExtractor) { - final Set seen = new HashSet<>(); - return value -> seen.add(keyExtractor.apply(value)); - } - private BidderUsersyncStatus validStatus(String bidder, CookieSyncContext cookieSyncContext) { final BiddersContext biddersContext = cookieSyncContext.getBiddersContext(); final RoutingContext routingContext = cookieSyncContext.getRoutingContext(); @@ -474,6 +491,8 @@ private BidderUsersyncStatus rejectionStatus(String bidder, RejectionReason reas case DISABLED_USERSYNC -> builder.conditionalError(requested || coopSync, "Sync disabled by config"); case REJECTED_BY_FILTER -> builder.conditionalError(requested || coopSync, "Rejected by request filter"); case ALREADY_IN_SYNC -> builder.conditionalError(requested, "Already in sync"); + case REJECTED_BY_REGULATION_SCOPE -> builder.conditionalError( + requested || coopSync, "Rejected by regulation scope"); }; return builder.build(); @@ -497,7 +516,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..7ee18499527 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); @@ -74,8 +74,4 @@ public boolean isPrioritizedFamily(String cookieFamilyName) { final String bidder = prioritizedCookieFamilyNameToBidderName.get(cookieFamilyName); return prioritizedBidders.contains(bidder); } - - public boolean hasPrioritizedBidders() { - return !prioritizedBidders.isEmpty(); - } } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookie.java b/src/main/java/org/prebid/server/cookie/UidsCookie.java index 274e1ad12c0..ce7354c45f5 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookie.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookie.java @@ -102,7 +102,7 @@ public UidsCookie updateOptout(boolean optout) { /** * Converts {@link Uids} to JSON string. */ - String toJson() { + public String toJson() { return mapper.encodeToString(uids); } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieService.java b/src/main/java/org/prebid/server/cookie/UidsCookieService.java index 7416abfdfa2..8bfe414884c 100644 --- a/src/main/java/org/prebid/server/cookie/UidsCookieService.java +++ b/src/main/java/org/prebid/server/cookie/UidsCookieService.java @@ -3,23 +3,26 @@ 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; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; 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.model.UpdateResult; import org.prebid.server.util.HttpUtil; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -34,7 +37,10 @@ public class UidsCookieService { private static final Logger logger = LoggerFactory.getLogger(UidsCookieService.class); private static final String COOKIE_NAME = "uids"; + private static final String COOKIE_NAME_FORMAT = "uids%d"; private static final int MIN_COOKIE_SIZE_BYTES = 500; + private static final int MIN_NUMBER_OF_UID_COOKIES = 1; + private static final int MAX_NUMBER_OF_UID_COOKIES = 30; private final String optOutCookieName; private final String optOutCookieValue; @@ -42,7 +48,9 @@ public class UidsCookieService { private final String hostCookieName; private final String hostCookieDomain; private final long ttlSeconds; + private final int maxCookieSizeBytes; + private final int numberOfUidCookies; private final PrioritizedCoopSyncProvider prioritizedCoopSyncProvider; private final Metrics metrics; @@ -55,6 +63,7 @@ public UidsCookieService(String optOutCookieName, String hostCookieDomain, int ttlDays, int maxCookieSizeBytes, + int numberOfUidCookies, PrioritizedCoopSyncProvider prioritizedCoopSyncProvider, Metrics metrics, JacksonMapper mapper) { @@ -64,6 +73,12 @@ public UidsCookieService(String optOutCookieName, "Configured cookie size is less than allowed minimum size of " + MIN_COOKIE_SIZE_BYTES); } + if (numberOfUidCookies < MIN_NUMBER_OF_UID_COOKIES || numberOfUidCookies > MAX_NUMBER_OF_UID_COOKIES) { + throw new IllegalArgumentException( + "Configured number of uid cookies should be in the range from %d to %d" + .formatted(MIN_NUMBER_OF_UID_COOKIES, MAX_NUMBER_OF_UID_COOKIES)); + } + this.optOutCookieName = optOutCookieName; this.optOutCookieValue = optOutCookieValue; this.hostCookieFamily = hostCookieFamily; @@ -71,6 +86,7 @@ public UidsCookieService(String optOutCookieName, this.hostCookieDomain = StringUtils.isNotBlank(hostCookieDomain) ? hostCookieDomain : null; this.ttlSeconds = Duration.ofDays(ttlDays).getSeconds(); this.maxCookieSizeBytes = maxCookieSizeBytes; + this.numberOfUidCookies = numberOfUidCookies; this.prioritizedCoopSyncProvider = Objects.requireNonNull(prioritizedCoopSyncProvider); this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); @@ -105,19 +121,12 @@ public UidsCookie parseFromRequest(HttpRequestContext httpRequest) { */ UidsCookie parseFromCookies(Map cookies) { final Uids parsedUids = parseUids(cookies); + final boolean isOptedOut = isOptedOut(cookies); - final Boolean optout; - final Map uidsMap; - - if (isOptedOut(cookies)) { - optout = true; - uidsMap = Collections.emptyMap(); - } else { - optout = parsedUids != null ? parsedUids.getOptout() : null; - uidsMap = enrichAndSanitizeUids(parsedUids, cookies); - } - - final Uids uids = Uids.builder().uids(uidsMap).optout(optout).build(); + final Uids uids = Uids.builder() + .uids(isOptedOut ? Collections.emptyMap() : enrichAndSanitizeUids(parsedUids, cookies)) + .optout(isOptedOut) + .build(); return new UidsCookie(uids, mapper); } @@ -125,37 +134,53 @@ UidsCookie parseFromCookies(Map cookies) { /** * Parses cookies {@link Map} and composes {@link Uids} model. */ - public Uids parseUids(Map cookies) { - if (cookies.containsKey(COOKIE_NAME)) { - final String cookieValue = cookies.get(COOKIE_NAME); + private Uids parseUids(Map cookies) { + final Map uids = new HashMap<>(); + + for (Map.Entry cookie : cookies.entrySet()) { + final String cookieKey = cookie.getKey(); + if (!cookieKey.startsWith(COOKIE_NAME)) { + continue; + } + try { - return mapper.decodeValue(Buffer.buffer(Base64.getUrlDecoder().decode(cookieValue)), Uids.class); + final Uids parsedUids = mapper.decodeValue( + Buffer.buffer(Base64.getUrlDecoder().decode(cookie.getValue())), Uids.class); + if (parsedUids != null && parsedUids.getUids() != null) { + parsedUids.getUids().forEach((key, value) -> uids.merge(key, value, (newValue, oldValue) -> + newValue.getExpires().compareTo(oldValue.getExpires()) > 0 ? newValue : oldValue)); + } } 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, cookie.getValue()); } } - return null; + + return Uids.builder().uids(uids).build(); } /** * Creates a {@link Cookie} with 'uids' as a name and encoded JSON string representing supplied {@link UidsCookie} * as a value. */ - public Cookie toCookie(UidsCookie uidsCookie) { - return makeCookie(uidsCookie); + public Cookie aliveCookie(String cookieName, UidsCookie uidsCookie) { + final String value = Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes()); + return makeCookie(cookieName, value, ttlSeconds); + } + + public Cookie aliveCookie(UidsCookie uidsCookie) { + return aliveCookie(COOKIE_NAME, uidsCookie); } - private int cookieBytesLength(UidsCookie uidsCookie) { - return makeCookie(uidsCookie).encode().getBytes().length; + public Cookie expiredCookie(String cookieName) { + return makeCookie(cookieName, StringUtils.EMPTY, 0); } - private Cookie makeCookie(UidsCookie uidsCookie) { - return Cookie - .cookie(COOKIE_NAME, Base64.getUrlEncoder().encodeToString(uidsCookie.toJson().getBytes())) + private Cookie makeCookie(String cookieName, String value, long maxAge) { + return Cookie.cookie(cookieName, value) .setPath("/") .setSameSite(CookieSameSite.NONE) .setSecure(true) - .setMaxAge(ttlSeconds) + .setMaxAge(maxAge) .setDomain(hostCookieDomain); } @@ -221,20 +246,18 @@ private static boolean facebookSentinelOrEmpty(Map.Entry /*** * Removes expired {@link Uids}, updates {@link UidsCookie} with new uid for family name according to priority - * and trims it to the limit */ - public UidsCookieUpdateResult updateUidsCookie(UidsCookie uidsCookie, String familyName, String uid) { - final UidsCookie initialCookie = trimToLimit(removeExpiredUids(uidsCookie)); // if already exceeded limit - - if (StringUtils.isBlank(uid)) { - return UidsCookieUpdateResult.unaltered(initialCookie.deleteUid(familyName)); - } else if (UidsCookie.isFacebookSentinel(familyName, uid)) { - // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. - // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. - return UidsCookieUpdateResult.unaltered(initialCookie); + public UpdateResult updateUidsCookie(UidsCookie uidsCookie, String familyName, String uid) { + final UidsCookie initialCookie = removeExpiredUids(uidsCookie); + + // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. + // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. + if (StringUtils.isBlank(uid) || UidsCookie.isFacebookSentinel(familyName, uid)) { + return UpdateResult.unaltered(initialCookie); } - return updateUidsCookieByPriority(initialCookie, familyName, uid); + final UidsCookie updatedCookie = initialCookie.updateUid(familyName, uid); + return UpdateResult.updated(updatedCookie); } private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { @@ -250,47 +273,58 @@ private static UidsCookie removeExpiredUids(UidsCookie uidsCookie) { return updatedCookie; } - private UidsCookieUpdateResult updateUidsCookieByPriority(UidsCookie uidsCookie, String familyName, String uid) { - final UidsCookie updatedCookie = uidsCookie.updateUid(familyName, uid); - if (!cookieExceededMaxLength(updatedCookie)) { - return UidsCookieUpdateResult.updated(updatedCookie); - } + public List splitUidsIntoCookies(UidsCookie uidsCookie) { + final Uids cookieUids = uidsCookie.getCookieUids(); + final Map uids = cookieUids.getUids(); + final boolean hasOptout = !uidsCookie.allowsSync(); - if (!prioritizedCoopSyncProvider.hasPrioritizedBidders() - || prioritizedCoopSyncProvider.isPrioritizedFamily(familyName)) { - return UidsCookieUpdateResult.updated(trimToLimit(updatedCookie)); - } else { - metrics.updateUserSyncSizeBlockedMetric(familyName); - return UidsCookieUpdateResult.unaltered(uidsCookie); - } - } + final Iterator cookieFamilies = cookieFamilyNamesByDescPriorityAndExpiration(uidsCookie); + final List splitCookies = new ArrayList<>(); - private boolean cookieExceededMaxLength(UidsCookie uidsCookie) { - return maxCookieSizeBytes > 0 && cookieBytesLength(uidsCookie) > maxCookieSizeBytes; - } + final int cookieSchemaSize = UidsCookieSize.schemaSize(makeCookie(COOKIE_NAME, StringUtils.EMPTY, ttlSeconds)); + String nextCookieFamily = null; + for (int i = 0; i < numberOfUidCookies; i++) { + final int digits = i < 10 ? Integer.signum(i) : 2; + final UidsCookieSize uidsCookieSize = new UidsCookieSize(cookieSchemaSize + digits, maxCookieSizeBytes); - private UidsCookie trimToLimit(UidsCookie uidsCookie) { - if (!cookieExceededMaxLength(uidsCookie)) { - return uidsCookie; - } + final Map tempUids = new HashMap<>(); + while (nextCookieFamily != null || cookieFamilies.hasNext()) { + nextCookieFamily = nextCookieFamily == null ? cookieFamilies.next() : nextCookieFamily; + final UidWithExpiry uidWithExpiry = uids.get(nextCookieFamily); - UidsCookie trimmedUids = uidsCookie; - final Iterator familyToRemoveIterator = cookieFamilyNamesByAscendingPriority(uidsCookie); + uidsCookieSize.addUid(nextCookieFamily, uidWithExpiry.getUid()); + if (!uidsCookieSize.isValid()) { + break; + } + + tempUids.put(nextCookieFamily, uidWithExpiry); + nextCookieFamily = null; + } + + final String uidsName = i == 0 ? COOKIE_NAME : COOKIE_NAME_FORMAT.formatted(i + 1); + + if (tempUids.isEmpty()) { + splitCookies.add(expiredCookie(uidsName)); + } else { + splitCookies.add(aliveCookie( + uidsName, + new UidsCookie(Uids.builder().uids(tempUids).optout(hasOptout).build(), mapper))); + } + } - while (familyToRemoveIterator.hasNext() && cookieExceededMaxLength(trimmedUids)) { - final String familyToRemove = familyToRemoveIterator.next(); - metrics.updateUserSyncSizedOutMetric(familyToRemove); - trimmedUids = trimmedUids.deleteUid(familyToRemove); + if (nextCookieFamily != null) { + updateSyncSizeMetrics(nextCookieFamily); } - return trimmedUids; + cookieFamilies.forEachRemaining(this::updateSyncSizeMetrics); + + return splitCookies; } - private Iterator cookieFamilyNamesByAscendingPriority(UidsCookie uidsCookie) { + private Iterator cookieFamilyNamesByDescPriorityAndExpiration(UidsCookie uidsCookie) { return uidsCookie.getCookieUids().getUids().entrySet().stream() .sorted(this::compareCookieFamilyNames) .map(Map.Entry::getKey) - .toList() .iterator(); } @@ -303,9 +337,17 @@ private int compareCookieFamilyNames(Map.Entry left, if ((leftPrioritized && rightPrioritized) || (!leftPrioritized && !rightPrioritized)) { return left.getValue().getExpires().compareTo(right.getValue().getExpires()); } else if (leftPrioritized) { - return 1; - } else { // right is prioritized return -1; + } else { // right is prioritized + return 1; + } + } + + private void updateSyncSizeMetrics(String nextCookieFamily) { + if (prioritizedCoopSyncProvider.isPrioritizedFamily(nextCookieFamily)) { + metrics.updateUserSyncSizedOutMetric(nextCookieFamily); + } else { + metrics.updateUserSyncSizeBlockedMetric(nextCookieFamily); } } diff --git a/src/main/java/org/prebid/server/cookie/UidsCookieSize.java b/src/main/java/org/prebid/server/cookie/UidsCookieSize.java new file mode 100644 index 00000000000..c98d74297b2 --- /dev/null +++ b/src/main/java/org/prebid/server/cookie/UidsCookieSize.java @@ -0,0 +1,73 @@ +package org.prebid.server.cookie; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.http.Cookie; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.json.ObjectMapperProvider; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class UidsCookieSize { + + // {"tempUIDs":{},"optout":false} + private static final int TEMP_UIDS_BASE64_BYTES = "eyJ0ZW1wVUlEcyI6e30sIm9wdG91dCI6ZmFsc2V9".length(); + private static final int UID_TEMPLATE_BYTES; + + static { + try { + UID_TEMPLATE_BYTES = "\"\":{\"uid\":\"\",\"expires\":\"%s\"}," + .formatted(ObjectMapperProvider.mapper().writeValueAsString( + ZonedDateTime.ofInstant(Instant.ofEpochSecond(0, 1), ZoneId.of("UTC")))) + .length(); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private final int cookieSchemaSize; + private final int maxSize; + private int encodedUidsSize; + + public UidsCookieSize(int cookieSchemaSize, int maxSize) { + this.cookieSchemaSize = cookieSchemaSize; + this.maxSize = maxSize; + + encodedUidsSize = 0; + } + + public static int schemaSize(Cookie cookieSchema) { + return cookieSchema.setValue(StringUtils.EMPTY).encode().length(); + } + + public boolean isValid() { + return maxSize <= 0 || totalSize() <= maxSize; + } + + public int totalSize() { + return cookieSchemaSize + + TEMP_UIDS_BASE64_BYTES + + Base64Size.base64Size(encodedUidsSize); + } + + public void addUid(String cookieFamily, String uid) { + final int uidSize = UID_TEMPLATE_BYTES + cookieFamily.length() + uid.length(); + encodedUidsSize = Base64Size.encodeSize(Base64Size.decodeSize(encodedUidsSize) + uidSize); + } + + private static class Base64Size { + + public static int encodeSize(int size) { + return size / 3 * 4 + size % 3; + } + + public static int decodeSize(int encodedSize) { + return encodedSize / 4 * 3 + encodedSize % 4; + } + + private static int base64Size(int encodedSize) { + return (encodedSize & -4) + 4 * Integer.signum(encodedSize % 4); + } + } +} diff --git a/src/main/java/org/prebid/server/cookie/model/BiddersContext.java b/src/main/java/org/prebid/server/cookie/model/BiddersContext.java index ef24f1b9085..98a90604adc 100644 --- a/src/main/java/org/prebid/server/cookie/model/BiddersContext.java +++ b/src/main/java/org/prebid/server/cookie/model/BiddersContext.java @@ -15,7 +15,7 @@ @Accessors(fluent = true) @Builder(toBuilder = true) -@Value(staticConstructor = "of") +@Value public class BiddersContext { @Builder.Default diff --git a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java index 886c245122c..281313d25f5 100644 --- a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java +++ b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java @@ -8,7 +8,7 @@ import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.bidder.UsersyncMethodChooser; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.request.CookieSyncRequest; import org.prebid.server.settings.model.Account; diff --git a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java index 8c503642c29..5130a3abb93 100644 --- a/src/main/java/org/prebid/server/cookie/model/RejectionReason.java +++ b/src/main/java/org/prebid/server/cookie/model/RejectionReason.java @@ -10,5 +10,6 @@ public enum RejectionReason { UNCONFIGURED_USERSYNC, DISABLED_USERSYNC, REJECTED_BY_FILTER, - ALREADY_IN_SYNC + ALREADY_IN_SYNC, + REJECTED_BY_REGULATION_SCOPE } diff --git a/src/main/java/org/prebid/server/cookie/model/UidWithExpiry.java b/src/main/java/org/prebid/server/cookie/model/UidWithExpiry.java index a518bcbb7bc..b93e9930030 100644 --- a/src/main/java/org/prebid/server/cookie/model/UidWithExpiry.java +++ b/src/main/java/org/prebid/server/cookie/model/UidWithExpiry.java @@ -1,6 +1,5 @@ package org.prebid.server.cookie.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.time.Clock; @@ -11,7 +10,6 @@ /** * Bundles the UID with an Expiration date. After the expiration, the UID is no longer valid. */ -@AllArgsConstructor @Value public class UidWithExpiry { diff --git a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java b/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java deleted file mode 100644 index 000b28c7018..00000000000 --- a/src/main/java/org/prebid/server/cookie/model/UidsCookieUpdateResult.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.cookie.model; - -import lombok.Value; -import org.prebid.server.cookie.UidsCookie; - -@Value(staticConstructor = "of") -public class UidsCookieUpdateResult { - - boolean successfullyUpdated; - - UidsCookie uidsCookie; - - public static UidsCookieUpdateResult updated(UidsCookie uidsCookie) { - return of(true, uidsCookie); - } - - public static UidsCookieUpdateResult unaltered(UidsCookie uidsCookie) { - return of(false, uidsCookie); - } -} diff --git a/src/main/java/org/prebid/server/currency/CurrencyConversionService.java b/src/main/java/org/prebid/server/currency/CurrencyConversionService.java index cc63eb5e927..efcaf4ad7c2 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(), + // chose the largest precision among intermediate rates + 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/currency/proto/CurrencyConversionRates.java b/src/main/java/org/prebid/server/currency/proto/CurrencyConversionRates.java index 464fd87a188..832f142dc87 100644 --- a/src/main/java/org/prebid/server/currency/proto/CurrencyConversionRates.java +++ b/src/main/java/org/prebid/server/currency/proto/CurrencyConversionRates.java @@ -1,7 +1,6 @@ package org.prebid.server.currency.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; @@ -10,8 +9,7 @@ /** * Represents Currency Server response containing currency conversion rates for specific date. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CurrencyConversionRates { @JsonProperty("dataAsOf") 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..140d3fd91a8 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() { } @@ -69,7 +67,7 @@ public static void validateBidId(RoutingContext routingContext) { public static void validateFormat(RoutingContext routingContext) { final String format = routingContext.request().params().get(FORMAT_PARAMETER); - if (StringUtils.isNotEmpty(format) && !format.equals(BLANK_FORMAT) && !format.equals(IMAGE_FORMAT)) { + if (StringUtils.isNotEmpty(format) && !BLANK_FORMAT.equals(format) && !IMAGE_FORMAT.equals(format)) { throw new IllegalArgumentException( "Format '%s' query parameter is invalid. Possible values are %s and %s, but was %s" .formatted(FORMAT_PARAMETER, BLANK_FORMAT, IMAGE_FORMAT, format)); @@ -78,8 +76,8 @@ public static void validateFormat(RoutingContext routingContext) { public static void validateAnalytics(RoutingContext routingContext) { final String analytics = routingContext.request().params().get(ANALYTICS_PARAMETER); - if (StringUtils.isNotEmpty(analytics) && !analytics.equals(ENABLED_ANALYTICS) - && !analytics.equals(DISABLED_ANALYTICS)) { + if (StringUtils.isNotEmpty(analytics) && !ENABLED_ANALYTICS.equals(analytics) + && !DISABLED_ANALYTICS.equals(analytics)) { throw new IllegalArgumentException( "Analytics '%s' query parameter is invalid. Possible values are %s and %s, but was %s" .formatted(ANALYTICS_PARAMETER, ENABLED_ANALYTICS, DISABLED_ANALYTICS, analytics)); @@ -118,7 +116,7 @@ public static EventRequest from(RoutingContext routingContext) { final MultiMap queryParams = routingContext.request().params(); final String typeAsString = queryParams.get(TYPE_PARAMETER); - final EventRequest.Type type = typeAsString.equals(WIN_TYPE) ? EventRequest.Type.win : EventRequest.Type.imp; + final EventRequest.Type type = WIN_TYPE.equals(typeAsString) ? EventRequest.Type.win : EventRequest.Type.imp; final EventRequest.Format format = Objects.equals(queryParams.get(FORMAT_PARAMETER), IMAGE_FORMAT) ? EventRequest.Format.image : EventRequest.Format.blank; @@ -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/InvalidProfileException.java b/src/main/java/org/prebid/server/exception/InvalidProfileException.java new file mode 100644 index 00000000000..d8a3ebc3aae --- /dev/null +++ b/src/main/java/org/prebid/server/exception/InvalidProfileException.java @@ -0,0 +1,14 @@ +package org.prebid.server.exception; + +import java.util.List; + +public class InvalidProfileException extends RuntimeException { + + public InvalidProfileException(String message) { + super(message); + } + + public InvalidProfileException(List messages) { + super(String.join("\n", messages)); + } +} 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/RemoteFileProcessor.java b/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java deleted file mode 100644 index 8621e00dbce..00000000000 --- a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.execution; - -import io.vertx.core.Future; - -/** - * Contract fro services which use external files. - */ -public interface RemoteFileProcessor { - - Future setDataPath(String dataFilePath); -} - diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java deleted file mode 100644 index f47faf082a7..00000000000 --- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java +++ /dev/null @@ -1,275 +0,0 @@ -package org.prebid.server.execution; - -import io.vertx.core.AsyncResult; -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.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 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.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 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; - - public RemoteFileSyncer(String downloadUrl, - String saveFilePath, - String tmpFilePath, - RetryPolicy retryPolicy, - long timeout, - long updatePeriod, - HttpClient httpClient, - Vertx vertx) { - - 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); - this.fileSystem = vertx.fileSystem(); - - createAndCheckWritePermissionsFor(fileSystem, saveFilePath); - createAndCheckWritePermissionsFor(fileSystem, tmpFilePath); - } - - private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { - try { - final String dirPath = Paths.get(filePath).getParent().toString(); - final FileProps props = fileSystem.existsBlocking(dirPath) ? fileSystem.propsBlocking(dirPath) : null; - if (props == null || !props.isDirectory()) { - fileSystem.mkdirsBlocking(dirPath); - } else if (!Files.isWritable(Paths.get(dirPath))) { - throw new PreBidException("No write permissions for directory: " + dirPath); - } - } catch (FileSystemException | InvalidPathException e) { - throw new PreBidException("Cannot create directory for file: " + filePath, e); - } - } - - 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(); - } - - private Future processSavedFile(RemoteFileProcessor processor) { - return processor.setDataPath(saveFilePath) - .map(false) - .recover(ignored -> removeCorruptedSaveFile()); - } - - 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 syncRemoteFiles(RetryPolicy retryPolicy) { - return deleteFileIfExists(tmpFilePath) - .compose(ignored -> downloadToTempFile()) - .recover(error -> retrySync(retryPolicy)) - .compose(downloadResult -> swapFiles()) - .map(true); - } - - private Future deleteFileIfExists(String filePath) { - return isFileExists(filePath) - .compose(exists -> exists ? deleteFile(filePath) : Future.succeededFuture()); - } - - private Future deleteFile(String filePath) { - final Promise promise = Promise.promise(); - fileSystem.delete(filePath, promise); - return promise.future(); - } - - private Future downloadToTempFile() { - return openFile(tmpFilePath) - .compose(tmpFile -> requestData() - .compose(response -> pumpToFile(response, tmpFile))); - } - - 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); - - final Promise promise = Promise.promise(); - vertx.setTimer(policy.delay(), timerId -> - syncRemoteFiles(policy.next()) - .onFailure(promise::fail) - .onSuccess(ignored -> promise.complete())); - - 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 - if (updatePeriod > 0) { - vertx.setTimer(updatePeriod, idUpdateNew -> configureAutoUpdates(remoteFileProcessor)); - } - } - - private void logFileProcessStatus(AsyncResult serviceRespond) { - if (serviceRespond.succeeded()) { - logger.info("Service successfully received file {0}.", saveFilePath); - } else { - logger.error("Service cant process file {0} and still unavailable.", saveFilePath); - } - } - - 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) { - 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); - } - } -} diff --git a/src/main/java/org/prebid/server/execution/file/FileProcessor.java b/src/main/java/org/prebid/server/execution/file/FileProcessor.java new file mode 100644 index 00000000000..f17ab4758ee --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileProcessor.java @@ -0,0 +1,8 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Future; + +public interface FileProcessor { + + Future setDataPath(String dataFilePath); +} diff --git a/src/main/java/org/prebid/server/execution/file/FileUtil.java b/src/main/java/org/prebid/server/execution/file/FileUtil.java new file mode 100644 index 00000000000..3de28c1992f --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/FileUtil.java @@ -0,0 +1,106 @@ +package org.prebid.server.execution.file; + +import io.vertx.core.Vertx; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +import io.vertx.core.file.FileSystemException; +import io.vertx.core.http.HttpClientOptions; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.syncer.FileSyncer; +import org.prebid.server.execution.file.syncer.LocalFileSyncer; +import org.prebid.server.execution.file.syncer.RemoteFileSyncerV2; +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.spring.config.model.ExponentialBackoffProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; +import org.prebid.server.spring.config.model.HttpClientProperties; + +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class FileUtil { + + private FileUtil() { + } + + public static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) { + try { + final Path dirPath = Paths.get(filePath).getParent(); + final String dirPathString = dirPath.toString(); + final FileProps props = fileSystem.existsBlocking(dirPathString) + ? fileSystem.propsBlocking(dirPathString) + : null; + + if (props == null || !props.isDirectory()) { + fileSystem.mkdirsBlocking(dirPathString); + } else if (!Files.isWritable(dirPath)) { + throw new PreBidException("No write permissions for directory: " + dirPath); + } + } catch (FileSystemException | InvalidPathException e) { + throw new PreBidException("Cannot create directory for file: " + filePath, e); + } + } + + public static FileSyncer fileSyncerFor(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + return switch (properties.getType()) { + case LOCAL -> new LocalFileSyncer( + fileProcessor, + properties.getSaveFilepath(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + case REMOTE -> remoteFileSyncer(fileProcessor, properties, vertx); + }; + } + + private static RemoteFileSyncerV2 remoteFileSyncer(FileProcessor fileProcessor, + FileSyncerProperties properties, + Vertx vertx) { + + final HttpClientProperties httpClientProperties = properties.getHttpClient(); + final HttpClientOptions httpClientOptions = new HttpClientOptions() + .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) + .setMaxRedirects(httpClientProperties.getMaxRedirects()); + + return new RemoteFileSyncerV2( + fileProcessor, + properties.getDownloadUrl(), + properties.getSaveFilepath(), + properties.getTmpFilepath(), + vertx.createHttpClient(httpClientOptions), + properties.getTimeoutMs(), + properties.isCheckSize(), + properties.getUpdateIntervalMs(), + toRetryPolicy(properties), + vertx); + } + + // TODO: remove after transition period + private static RetryPolicy toRetryPolicy(FileSyncerProperties 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()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java new file mode 100644 index 00000000000..55517caa9a7 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java @@ -0,0 +1,47 @@ +package org.prebid.server.execution.file.supplier; + +import io.vertx.core.Future; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class LocalFileSupplier implements Supplier> { + + private final String filePath; + private final FileSystem fileSystem; + private final AtomicLong lastSupplyTime; + + public LocalFileSupplier(String filePath, FileSystem fileSystem) { + this.filePath = Objects.requireNonNull(filePath); + this.fileSystem = Objects.requireNonNull(fileSystem); + lastSupplyTime = new AtomicLong(Long.MIN_VALUE); + } + + @Override + public Future get() { + return fileSystem.exists(filePath) + .compose(exists -> exists + ? fileSystem.props(filePath) + : Future.failedFuture("File %s not found.".formatted(filePath))) + .map(this::getFileIfModified); + } + + private String getFileIfModified(FileProps fileProps) { + final long lastModifiedTime = lasModifiedTime(fileProps); + final long lastSupplyTime = this.lastSupplyTime.get(); + + if (lastSupplyTime < lastModifiedTime) { + this.lastSupplyTime.compareAndSet(lastSupplyTime, lastModifiedTime); + return filePath; + } + + return null; + } + + private static long lasModifiedTime(FileProps fileProps) { + return Math.max(fileProps.creationTime(), fileProps.lastModifiedTime()); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java new file mode 100644 index 00000000000..855ee4e8f9d --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java @@ -0,0 +1,160 @@ +package org.prebid.server.execution.file.supplier; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.file.CopyOptions; +import io.vertx.core.file.FileProps; +import io.vertx.core.file.FileSystem; +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.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; + +import java.util.Objects; +import java.util.function.Supplier; + +public class RemoteFileSupplier implements Supplier> { + + private static final Logger logger = LoggerFactory.getLogger(RemoteFileSupplier.class); + + private final String savePath; + private final String backupPath; + private final String tmpPath; + private final HttpClient httpClient; + private final FileSystem fileSystem; + + private final RequestOptions getRequestOptions; + private final RequestOptions headRequestOptions; + + public RemoteFileSupplier(String downloadUrl, + String savePath, + String tmpPath, + HttpClient httpClient, + long timeout, + boolean checkRemoteFileSize, + FileSystem fileSystem) { + + this.savePath = Objects.requireNonNull(savePath); + this.backupPath = savePath + ".old"; + this.tmpPath = Objects.requireNonNull(tmpPath); + this.httpClient = Objects.requireNonNull(httpClient); + this.fileSystem = Objects.requireNonNull(fileSystem); + + HttpUtil.validateUrl(downloadUrl); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, savePath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, backupPath); + FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpPath); + + getRequestOptions = new RequestOptions() + .setMethod(HttpMethod.GET) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true); + headRequestOptions = checkRemoteFileSize + ? new RequestOptions() + .setMethod(HttpMethod.HEAD) + .setTimeout(timeout) + .setAbsoluteURI(downloadUrl) + .setFollowRedirects(true) + : null; + } + + @Override + public Future get() { + return isDownloadRequired().compose(isDownloadRequired -> isDownloadRequired + ? Future.all(downloadFile(), createBackup()) + .compose(ignored -> tmpToSave()) + .map(savePath) + : Future.succeededFuture()); + } + + private Future isDownloadRequired() { + return headRequestOptions != null + ? fileSystem.exists(savePath) + .compose(exists -> exists ? isSizeChanged() : Future.succeededFuture(true)) + : Future.succeededFuture(true); + } + + private Future isSizeChanged() { + final Future localFileSize = fileSystem.props(savePath).map(FileProps::size); + final Future remoteFileSize = sendHttpRequest(headRequestOptions) + .map(response -> response.getHeader(HttpHeaders.CONTENT_LENGTH)) + .map(Long::parseLong); + + return Future.all(localFileSize, remoteFileSize) + .map(compositeResult -> !Objects.equals(compositeResult.resultAt(0), compositeResult.resultAt(1))); + } + + private Future downloadFile() { + return fileSystem.open(tmpPath, new OpenOptions()) + .compose(tmpFile -> sendHttpRequest(getRequestOptions) + .onFailure(ignored -> tmpFile.close()) + .compose(response -> response.pipeTo(tmpFile))); + } + + private Future sendHttpRequest(RequestOptions requestOptions) { + return httpClient.request(requestOptions) + .compose(HttpClientRequest::send) + .map(this::validateResponse); + } + + private HttpClientResponse validateResponse(HttpClientResponse response) { + final int statusCode = response.statusCode(); + if (statusCode != HttpResponseStatus.OK.code()) { + throw new PreBidException("Got unexpected response from server with status code %s and message %s" + .formatted(statusCode, response.statusMessage())); + } + + return response; + } + + private Future tmpToSave() { + return copyFile(tmpPath, savePath); + } + + public void clearTmp() { + fileSystem.exists(tmpPath).onSuccess(exists -> { + if (exists) { + deleteFile(tmpPath); + } + }); + } + + private Future createBackup() { + return fileSystem.exists(savePath) + .compose(exists -> exists ? copyFile(savePath, backupPath) : Future.succeededFuture()); + } + + public void deleteBackup() { + fileSystem.exists(backupPath).onSuccess(exists -> { + if (exists) { + deleteFile(backupPath); + } + }); + } + + public Future restoreFromBackup() { + return fileSystem.exists(backupPath) + .compose(exists -> exists + ? copyFile(backupPath, savePath) + .onSuccess(ignored -> deleteFile(backupPath)) + : Future.succeededFuture()); + } + + private Future copyFile(String from, String to) { + return fileSystem.move(from, to, new CopyOptions().setReplaceExisting(true)); + } + + private void deleteFile(String filePath) { + fileSystem.delete(filePath) + .onFailure(error -> logger.error("Can't delete file: " + filePath)); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java new file mode 100644 index 00000000000..fd850e126c4 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java @@ -0,0 +1,84 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +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 java.util.Objects; +import java.util.function.Function; + +public abstract class FileSyncer { + + private static final Logger logger = LoggerFactory.getLogger(FileSyncer.class); + + private final FileProcessor fileProcessor; + private final long updatePeriod; + private final RetryPolicy retryPolicy; + private final Vertx vertx; + + protected FileSyncer(FileProcessor fileProcessor, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + this.fileProcessor = Objects.requireNonNull(fileProcessor); + this.updatePeriod = updatePeriod; + this.retryPolicy = Objects.requireNonNull(retryPolicy); + this.vertx = Objects.requireNonNull(vertx); + } + + public void sync() { + sync(retryPolicy); + } + + private void sync(RetryPolicy currentRetryPolicy) { + getFile() + .compose(this::processFile) + .onSuccess(ignored -> onSuccess()) + .onFailure(failure -> onFailure(currentRetryPolicy, failure)); + } + + protected abstract Future getFile(); + + private Future processFile(String filePath) { + return filePath != null + ? vertx.executeBlocking(() -> fileProcessor.setDataPath(filePath)) + .compose(Function.identity()) + .onFailure(error -> logger.error("Can't process saved file: " + filePath)) + : Future.succeededFuture(); + } + + private void onSuccess() { + doOnSuccess().onComplete(ignored -> setUpDeferredUpdate()); + } + + protected abstract Future doOnSuccess(); + + private void setUpDeferredUpdate() { + if (updatePeriod > 0) { + vertx.setTimer(updatePeriod, ignored -> sync()); + } + } + + private void onFailure(RetryPolicy currentRetryPolicy, Throwable failure) { + doOnFailure(failure).onComplete(ignored -> retrySync(currentRetryPolicy)); + } + + protected abstract Future doOnFailure(Throwable throwable); + + private void retrySync(RetryPolicy currentRetryPolicy) { + if (currentRetryPolicy instanceof Retryable policy) { + logger.info( + "Retrying file sync for {} with policy: {}", + fileProcessor.getClass().getSimpleName(), + policy); + vertx.setTimer(policy.delay(), timerId -> sync(policy.next())); + } else { + setUpDeferredUpdate(); + } + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java new file mode 100644 index 00000000000..6ea109185b5 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java @@ -0,0 +1,38 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class LocalFileSyncer extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + + public LocalFileSyncer(FileProcessor fileProcessor, + String localFile, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + localFileSupplier = new LocalFileSupplier(localFile, vertx.fileSystem()); + } + + @Override + protected Future getFile() { + return localFileSupplier.get(); + } + + @Override + protected Future doOnSuccess() { + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + return Future.succeededFuture(); + } +} diff --git a/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java new file mode 100644 index 00000000000..e4f8c81ec46 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java @@ -0,0 +1,175 @@ +package org.prebid.server.execution.file.syncer; + +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.CopyOptions; +import io.vertx.core.file.FileSystem; +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.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.file.FileProcessor; +import org.prebid.server.execution.file.FileUtil; +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.util.Objects; +import java.util.function.Function; + +@Deprecated(forRemoval = true) +public class RemoteFileSyncer { + + private static final Logger logger = LoggerFactory.getLogger(RemoteFileSyncer.class); + + private final FileProcessor processor; + private final String downloadUrl; + private final String saveFilePath; + private final String tmpFilePath; + private final RetryPolicy retryPolicy; + 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(FileProcessor processor, + String downloadUrl, + String saveFilePath, + String tmpFilePath, + RetryPolicy retryPolicy, + long timeout, + long updatePeriod, + 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.updatePeriod = updatePeriod; + this.httpClient = Objects.requireNonNull(httpClient); + this.vertx = Objects.requireNonNull(vertx); + this.fileSystem = vertx.fileSystem(); + + FileUtil.createAndCheckWritePermissionsFor(fileSystem, saveFilePath); + FileUtil.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); + } + + public void sync() { + fileSystem.exists(saveFilePath) + .compose(exists -> exists ? processSavedFile() : syncRemoteFile(retryPolicy)) + .onComplete(ignored -> setUpDeferredUpdate()); + } + + private Future processSavedFile() { + return vertx.executeBlocking(() -> processor.setDataPath(saveFilePath)) + .compose(Function.identity()) + .onFailure(error -> logger.error("Can't process saved file: " + saveFilePath)) + .recover(ignored -> deleteFile(saveFilePath).mapEmpty()) + .mapEmpty(); + } + + private Future deleteFile(String filePath) { + return fileSystem.delete(filePath) + .onFailure(error -> logger.error("Can't delete corrupted file: " + saveFilePath)); + } + + private Future syncRemoteFile(RetryPolicy retryPolicy) { + return fileSystem.open(tmpFilePath, new OpenOptions()) + + .compose(tmpFile -> sendHttpRequest(getFileRequestOptions) + .compose(response -> response.pipeTo(tmpFile)) + .onComplete(result -> tmpFile.close())) + + .compose(ignored -> fileSystem.move( + tmpFilePath, saveFilePath, new CopyOptions().setReplaceExisting(true))) + + .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 retrySync(RetryPolicy retryPolicy) { + if (retryPolicy instanceof Retryable policy) { + logger.info("Retrying file download from {} with policy: {}", downloadUrl, retryPolicy); + + final Promise promise = Promise.promise(); + vertx.setTimer(policy.delay(), timerId -> syncRemoteFile(policy.next()).onComplete(promise)); + return promise.future(); + } else { + return Future.failedFuture(new PreBidException("File sync failed")); + } + } + + private void setUpDeferredUpdate() { + if (updatePeriod > 0) { + vertx.setPeriodic(updatePeriod, ignored -> updateIfNeeded()); + } + } + + 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 { + return Future.succeededFuture(response); + } + } + + private Future isLengthChanged(HttpClientResponse response) { + final String contentLengthParameter = response.getHeader(HttpHeaders.CONTENT_LENGTH); + return StringUtils.isNumeric(contentLengthParameter) && !"0".equals(contentLengthParameter) + ? 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/execution/file/syncer/RemoteFileSyncerV2.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java new file mode 100644 index 00000000000..54755dccc19 --- /dev/null +++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java @@ -0,0 +1,69 @@ +package org.prebid.server.execution.file.syncer; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.file.FileSystem; +import io.vertx.core.http.HttpClient; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.file.supplier.LocalFileSupplier; +import org.prebid.server.execution.file.supplier.RemoteFileSupplier; +import org.prebid.server.execution.retry.RetryPolicy; + +public class RemoteFileSyncerV2 extends FileSyncer { + + private final LocalFileSupplier localFileSupplier; + private final RemoteFileSupplier remoteFileSupplier; + + public RemoteFileSyncerV2(FileProcessor fileProcessor, + String downloadUrl, + String saveFilePath, + String tmpFilePath, + HttpClient httpClient, + long timeout, + boolean checkSize, + long updatePeriod, + RetryPolicy retryPolicy, + Vertx vertx) { + + super(fileProcessor, updatePeriod, retryPolicy, vertx); + + final FileSystem fileSystem = vertx.fileSystem(); + localFileSupplier = new LocalFileSupplier(saveFilePath, fileSystem); + remoteFileSupplier = new RemoteFileSupplier( + downloadUrl, + saveFilePath, + tmpFilePath, + httpClient, + timeout, + checkSize, + fileSystem); + } + + @Override + protected Future getFile() { + return localFileSupplier.get() + .otherwiseEmpty() + .compose(localFile -> localFile != null + ? Future.succeededFuture(localFile) + : remoteFileSupplier.get()); + } + + @Override + protected Future doOnSuccess() { + remoteFileSupplier.clearTmp(); + remoteFileSupplier.deleteBackup(); + forceLastSupplyTimeUpdate(); + return Future.succeededFuture(); + } + + @Override + protected Future doOnFailure(Throwable throwable) { + remoteFileSupplier.clearTmp(); + return remoteFileSupplier.restoreFromBackup() + .onSuccess(ignore -> forceLastSupplyTimeUpdate()); + } + + private void forceLastSupplyTimeUpdate() { + localFileSupplier.get(); + } +} diff --git a/src/main/java/org/prebid/server/execution/retry/FixedIntervalRetryPolicy.java b/src/main/java/org/prebid/server/execution/retry/FixedIntervalRetryPolicy.java index 7f000e49005..856867e984f 100644 --- a/src/main/java/org/prebid/server/execution/retry/FixedIntervalRetryPolicy.java +++ b/src/main/java/org/prebid/server/execution/retry/FixedIntervalRetryPolicy.java @@ -30,4 +30,3 @@ public RetryPolicy next() { : NonRetryable.instance(); } } - diff --git a/src/main/java/org/prebid/server/execution/Timeout.java b/src/main/java/org/prebid/server/execution/timeout/Timeout.java similarity index 95% rename from src/main/java/org/prebid/server/execution/Timeout.java rename to src/main/java/org/prebid/server/execution/timeout/Timeout.java index f5abf239c87..b0f37e439fc 100644 --- a/src/main/java/org/prebid/server/execution/Timeout.java +++ b/src/main/java/org/prebid/server/execution/timeout/Timeout.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import lombok.Getter; diff --git a/src/main/java/org/prebid/server/execution/TimeoutFactory.java b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java similarity index 95% rename from src/main/java/org/prebid/server/execution/TimeoutFactory.java rename to src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java index cbe2768af1a..ae2624c8585 100644 --- a/src/main/java/org/prebid/server/execution/TimeoutFactory.java +++ b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java @@ -1,4 +1,4 @@ -package org.prebid.server.execution; +package org.prebid.server.execution.timeout; import java.time.Clock; diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java index 0dd52cb8ff7..d943c6b92e9 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java @@ -3,7 +3,10 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -19,35 +22,75 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.EnumSet; +import java.util.List; import java.util.Objects; +import java.util.Optional; 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 final FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; + private final FloorAdjustmentsResolver floorAdjustmentsResolver; + + public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { - public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { this.floorAdjustmentFactorResolver = Objects.requireNonNull(floorAdjustmentFactorResolver); + this.floorAdjustmentsResolver = Objects.requireNonNull(floorAdjustmentsResolver); } @Override - public BigDecimal adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - final ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); + public Price adjustForImp(Imp imp, + String bidder, + BidRequest bidRequest, + Account account, + List debugWarnings) { + + final ExtRequestBidAdjustmentFactors bidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); final BigDecimal impBidFloor = imp.getBidfloor(); - if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null || extractBidAdjustmentFactors == null) { - return impBidFloor; + if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null) { + return Price.of(imp.getBidfloorcur(), impBidFloor); } - final Set impMediaTypes = retrieveImpMediaTypes(imp); - final BigDecimal factor = floorAdjustmentFactorResolver.resolve( - impMediaTypes, extractBidAdjustmentFactors, bidder); + final Set mediaTypes = retrieveImpMediaTypes(imp); + final Price adjustedBidFloor = adjustPrice(imp, bidder, impBidFloor, bidAdjustmentFactors, mediaTypes); + + try { + return floorAdjustmentsResolver.resolve(adjustedBidFloor, bidRequest, mediaTypes, bidder); + } catch (PreBidException e) { + return adjustedBidFloor; + } + } - return factor != null - ? BidderUtil.roundFloor(impBidFloor.divide(factor, ADJUSTMENT_SCALE, RoundingMode.HALF_EVEN)) + private Price adjustPrice(Imp imp, + String bidder, + BigDecimal impBidFloor, + ExtRequestBidAdjustmentFactors bidAdjustmentFactors, + Set mediaTypes) { + + if (bidAdjustmentFactors == null) { + return Price.of(imp.getBidfloorcur(), impBidFloor); + } + + final BigDecimal factor = floorAdjustmentFactorResolver.resolve(mediaTypes, bidAdjustmentFactors, bidder); + final BigDecimal adjustedBidFloorValue = factor != null && factor.compareTo(BigDecimal.ONE) != 0 + ? BidderUtil.roundFloor(DIVIDE_FUNCTION.apply(impBidFloor, factor)) : impBidFloor; + + return Price.of(imp.getBidfloorcur(), adjustedBidFloorValue); + } + + private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustmentfactors) + .orElse(null); } private static boolean shouldAdjustBidFloor(BidRequest bidRequest, Account account) { @@ -68,7 +111,7 @@ private static Set retrieveImpMediaTypes(Imp imp) { if (imp.getVideo() != null) { final Integer placement = imp.getVideo().getPlacement(); if (placement == null || Objects.equals(placement, 1)) { - availableMediaTypes.add(ImpMediaType.video); + availableMediaTypes.add(ImpMediaType.video_instream); } else { availableMediaTypes.add(ImpMediaType.video_outstream); } @@ -99,11 +142,4 @@ private static Boolean shouldAdjustBidFloorByAccount(Account account) { return ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getAdjustForBidAdjustment); } - - private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extPrebid = ObjectUtil.getIfNotNull(extRequest, ExtRequest::getPrebid); - - return ObjectUtil.getIfNotNull(extPrebid, ExtRequestPrebid::getBidadjustmentfactors); - } } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 9e8734576bb..2252a8fad54 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -1,12 +1,8 @@ package org.prebid.server.floors; 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; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -15,15 +11,19 @@ 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.BidRejection; 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; @@ -35,7 +35,9 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @@ -51,7 +53,9 @@ public class BasicPriceFloorEnforcer implements PriceFloorEnforcer { private final CurrencyConversionService currencyConversionService; private final Metrics metrics; - public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, Metrics metrics) { + public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, + Metrics metrics) { + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.metrics = Objects.requireNonNull(metrics); } @@ -135,7 +139,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 +152,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 +163,16 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, } final BigDecimal price = bid.getPrice(); - final BigDecimal floor = resolveFloor(bidderBid, bidderBidRequest, bidRequest, errors); + final Map originalPriceFloors = Optional.ofNullable(auctionParticipation.getBidderRequest()) + .map(BidderRequest::getOriginalPriceFloors) + .orElse(Collections.emptyMap()); + + final BigDecimal floor = resolveFloor( + originalPriceFloors, + bidderBid, + bidderBidRequest, + bidRequest, + errors); if (isPriceBelowFloor(price, floor)) { final String impId = bid.getImpid(); @@ -165,7 +180,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(BidRejection.of(bidderBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR)); updatedBidderBids.remove(bidderBid); } } @@ -197,7 +212,8 @@ private static boolean enforceDealFloors(AuctionParticipation auctionParticipati return BooleanUtils.isTrue(requestEnforceDealFloors) && BooleanUtils.isTrue(accountEnforceDealFloors); } - private BigDecimal resolveFloor(BidderBid bidderBid, + private BigDecimal resolveFloor(Map originalPriceFloors, + BidderBid bidderBid, BidRequest bidderBidRequest, BidRequest bidRequest, List errors) { @@ -210,9 +226,16 @@ private BigDecimal resolveFloor(BidderBid bidderBid, return convertIfRequired(customBidderFloor, priceFloorInfo.getCurrency(), bidderBidRequest, bidRequest); } - final Imp imp = correspondingImp(bidderBid.getBid(), bidRequest.getImp()); final String bidRequestCurrency = resolveBidRequestCurrency(bidRequest); - return convertCurrency(imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), bidRequestCurrency); + final Price originalFloorPrice = originalPriceFloors.get(bidderBid.getBid().getImpid()); + + return originalFloorPrice == null + ? null + : convertCurrency( + originalFloorPrice.getValue(), + bidRequest, + originalFloorPrice.getCurrency(), + bidRequestCurrency); } catch (PreBidException e) { final String logMessage = "Price floors enforcement failed for request id: %s, reason: %s" .formatted(bidRequest.getId(), e.getMessage()); @@ -261,16 +284,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); - } - - private static Imp correspondingImp(Bid bid, List imps) { - final String impId = bid.getImpid(); - return ListUtils.emptyIfNull(imps).stream() - .filter(imp -> Objects.equals(impId, imp.getId())) - .findFirst() - // Should never happen, see ResponseBidValidator usage. - .orElseThrow(() -> new PreBidException("Bid with impId %s doesn't have matched imp".formatted(impId))); + return CollectionUtils.isEmpty(currencies) ? null : currencies.getFirst(); } private static boolean isPriceBelowFloor(BigDecimal price, BigDecimal bidFloor) { diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java index f5cf62aeb72..6389139a5ac 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,10 @@ 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.metric.MetricName; +import org.prebid.server.metric.Metrics; 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; @@ -37,6 +38,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; public class BasicPriceFloorProcessor implements PriceFloorProcessor { @@ -46,22 +48,39 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor { private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private static final int MODEL_WEIGHT_MAX_VALUE = 100; private static final int MODEL_WEIGHT_MIN_VALUE = 1; + private static final String FETCH_FAILED_ERROR_MESSAGE = "Price floors processing failed: %s. " + + "Following parsing of request price floors is failed: %s"; + private static final String DYNAMIC_DATA_NOT_ALLOWED_MESSAGE = + "Price floors processing failed: Using dynamic data is not allowed. " + + "Following parsing of request price floors is failed: %s"; + private static final String INVALID_REQUEST_WARNING_MESSAGE = + "Price floors processing failed: parsing of request price floors is failed: %s"; + private static final String ERROR_LOG_MESSAGE = + "Price Floors can't be resolved for account %s and request %s, reason: %s"; + private final PriceFloorFetcher floorFetcher; private final PriceFloorResolver floorResolver; + private final Metrics metrics; private final JacksonMapper mapper; + private final double logSamplingRate; private final RandomWeightedEntrySupplier modelPicker; public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - JacksonMapper mapper) { + Metrics metrics, + JacksonMapper mapper, + double logSamplingRate) { this.floorFetcher = Objects.requireNonNull(floorFetcher); this.floorResolver = Objects.requireNonNull(floorResolver); + this.metrics = Objects.requireNonNull(metrics); this.mapper = Objects.requireNonNull(mapper); + this.logSamplingRate = logSamplingRate; modelPicker = new RandomPositiveWeightedEntrySupplier<>(BasicPriceFloorProcessor::resolveModelGroupWeight); } @@ -71,20 +90,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); + final PriceFloorRules floors = resolveFloors(account, bidRequest, warnings); + return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings); } private static boolean isPriceFloorsDisabled(Account account, BidRequest bidRequest) { @@ -123,45 +140,85 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) { return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors); } - private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List errors) { + private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List warnings) { final PriceFloorRules requestFloors = extractRequestFloors(bidRequest); final FetchResult fetchResult = floorFetcher.fetch(account); - final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus); + final FetchStatus fetchStatus = fetchResult.getFetchStatus(); + + final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getPriceFloors) + .map(AccountPriceFloorsConfig::getUseDynamicData) + .map(BooleanUtils::isNotFalse) + .orElse(true); - if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) { + final boolean shouldUseDynamicData = Optional.ofNullable(fetchResult.getRulesData()) + .map(PriceFloorData::getUseFetchDataRate) + .map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate) + .orElse(true); + + if (fetchStatus == FetchStatus.success && isUsingDynamicDataAllowed && shouldUseDynamicData) { final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData()); return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch); } - if (requestFloors != null) { - try { - PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE); - return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request); - } catch (PreBidException e) { - errors.add("Failed to parse price floors from request, with a reason : %s ".formatted(e.getMessage())); - conditionalLogger.error( - "Failed to parse price floors from request with id: '%s', with a reason : %s " - .formatted(bidRequest.getId(), e.getMessage()), - 0.01d); - } - } - - return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData); + return requestFloors == null + ? createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData) + : getPriceFloorRules( + bidRequest, account, requestFloors, fetchResult, isUsingDynamicDataAllowed, warnings); } - private static boolean shouldUseDynamicData(Account account) { - final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction); - final AccountPriceFloorsConfig floorsConfig = - ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors); + private PriceFloorRules getPriceFloorRules(BidRequest bidRequest, + Account account, + PriceFloorRules requestFloors, + FetchResult fetchResult, + boolean isDynamicDataAllowed, + List warnings) { - return BooleanUtils.isNotFalse( - ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getUseDynamicData)); + try { + final Optional priceFloorsConfig = Optional.of(account.getAuction()) + .map(AccountAuctionConfig::getPriceFloors); + + final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules) + .orElse(null); + final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims) + .orElse(null); + + PriceFloorRulesValidator.validateRules( + requestFloors, + PriceFloorsConfigResolver.resolveMaxValue(maxRules), + PriceFloorsConfigResolver.resolveMaxValue(maxDimensions)); + + return createFloorsFrom(requestFloors, fetchResult.getFetchStatus(), PriceFloorLocation.request); + } catch (PreBidException e) { + logErrorMessage(fetchResult, isDynamicDataAllowed, e, account.getId(), bidRequest.getId(), warnings); + return createFloorsFrom(null, fetchResult.getFetchStatus(), PriceFloorLocation.noData); + } } - private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, - PriceFloorData providerRulesData) { + private void logErrorMessage(FetchResult fetchResult, + boolean isDynamicDataAllowed, + PreBidException requestFloorsValidationException, + String accountId, + String requestId, + List warnings) { + + final String validationMessage = requestFloorsValidationException.getMessage(); + final String errorMessage = switch (fetchResult.getFetchStatus()) { + case inprogress -> null; + case error, timeout, none -> FETCH_FAILED_ERROR_MESSAGE.formatted( + fetchResult.getErrorMessage(), validationMessage); + case success -> isDynamicDataAllowed ? null : DYNAMIC_DATA_NOT_ALLOWED_MESSAGE.formatted(validationMessage); + }; + + if (errorMessage != null) { + warnings.add(INVALID_REQUEST_WARNING_MESSAGE.formatted(validationMessage)); + conditionalLogger.error(ERROR_LOG_MESSAGE.formatted(accountId, requestId, errorMessage), logSamplingRate); + metrics.updateAlertsMetrics(MetricName.general); + } + } + private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, PriceFloorData providerRulesData) { final Price floorMinPrice = resolveFloorMinPrice(requestFloors); return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder()) @@ -242,6 +299,7 @@ private static String resolveFloorProvider(PriceFloorRules rules) { } private BidRequest updateBidRequestWithFloors(BidRequest bidRequest, + String bidder, PriceFloorRules floors, List errors, List warnings) { @@ -251,7 +309,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 +349,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 +374,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 +386,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..b1273311049 --- /dev/null +++ b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java @@ -0,0 +1,74 @@ +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(List::getFirst) + .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)); + } + + 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..0654e6b523f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java @@ -2,14 +2,15 @@ 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); static NoOpPriceFloorAdjuster noOp() { return new NoOpPriceFloorAdjuster(); @@ -18,8 +19,13 @@ 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())); } } } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java index fd4e4ef9ab5..b1cc8c257b2 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; @@ -15,13 +13,15 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.HttpStatus; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.model.PriceFloorData; import org.prebid.server.floors.model.PriceFloorDebugProperties; import org.prebid.server.floors.proto.FetchResult; 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; @@ -90,7 +90,10 @@ public FetchResult fetch(Account account) { final AccountFetchContext accountFetchContext = fetchedData.get(account.getId()); return accountFetchContext != null - ? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus()) + ? FetchResult.of( + accountFetchContext.getRulesData(), + accountFetchContext.getFetchStatus(), + accountFetchContext.getErrorMessage()) : fetchPriceFloorData(account); } @@ -99,20 +102,20 @@ private FetchResult fetchPriceFloorData(Account account) { final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled); if (BooleanUtils.isFalse(fetchEnabled)) { - return FetchResult.of(null, FetchStatus.none); + return FetchResult.none("Fetching is disabled"); } final String accountId = account.getId(); final String fetchUrl = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getUrl); if (!isUrlValid(fetchUrl)) { - logger.error("Malformed fetch.url: '%s', passed for account %s".formatted(fetchUrl, accountId)); - return FetchResult.of(null, FetchStatus.error); + logger.error("Malformed fetch.url: '%s' passed for account %s".formatted(fetchUrl, accountId)); + return FetchResult.error("Malformed fetch.url '%s' passed".formatted(fetchUrl)); } if (!fetchInProgress.contains(accountId)) { fetchPriceFloorDataAsynchronous(fetchConfig, accountId); } - return FetchResult.of(null, FetchStatus.inprogress); + return FetchResult.inProgress(); } private boolean isUrlValid(String url) { @@ -137,18 +140,18 @@ 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); httpClient.get(fetchUrl, timeout, resolveMaxFileSize(maxFetchFileSizeKb)) - .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId)) + .map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig)) .recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId)) .map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId)) .map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId)); @@ -159,47 +162,42 @@ private static long resolveMaxFileSize(Long maxSizeInKBytes) { } private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientResponse, - AccountPriceFloorsFetchConfig fetchConfig, - String accountId) { + AccountPriceFloorsFetchConfig fetchConfig) { final int statusCode = httpClientResponse.getStatusCode(); if (statusCode != HttpStatus.SC_OK) { - throw new PreBidException("Failed to request for account %s, provider respond with status %s" - .formatted(accountId, statusCode)); + throw new PreBidException("Failed to request, provider respond with status %s".formatted(statusCode)); } final String body = httpClientResponse.getBody(); if (StringUtils.isBlank(body)) { - throw new PreBidException( - "Failed to parse price floor response for account %s, response body can not be empty" - .formatted(accountId)); + throw new PreBidException("Failed to parse price floor response, response body can not be empty"); } - final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId); - PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules())); + final PriceFloorData priceFloorData = parsePriceFloorData(body); + + PriceFloorRulesValidator.validateRulesData( + priceFloorData, + PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()), + PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDims())); return ResponseCacheInfo.of(priceFloorData, FetchStatus.success, + null, cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl())); } - private PriceFloorData parsePriceFloorData(String body, String accountId) { + private PriceFloorData parsePriceFloorData(String body) { final PriceFloorData priceFloorData; try { priceFloorData = mapper.decodeValue(body, PriceFloorData.class); } catch (DecodeException e) { - throw new PreBidException("Failed to parse price floor response for account %s, cause: %s" - .formatted(accountId, ExceptionUtils.getMessage(e))); + throw new PreBidException( + "Failed to parse price floor response, cause: %s".formatted(ExceptionUtils.getMessage(e))); } return priceFloorData; } - private static int resolveMaxRules(Long accountMaxRules) { - return accountMaxRules != null && !accountMaxRules.equals(0L) - ? Math.toIntExact(accountMaxRules) - : Integer.MAX_VALUE; - } - private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String fetchUrl) { final String cacheControlValue = httpClientResponse.getHeaders().get(HttpHeaders.CACHE_CONTROL); final Matcher cacheHeaderMatcher = StringUtils.isNotBlank(cacheControlValue) @@ -223,8 +221,11 @@ private PriceFloorData updateCache(ResponseCacheInfo cacheInfo, String accountId) { final long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig)); - final AccountFetchContext fetchContext = - AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId); + final AccountFetchContext fetchContext = AccountFetchContext.of( + cacheInfo.getRulesData(), + cacheInfo.getFetchStatus(), + cacheInfo.getErrorMessage(), + maxAgeTimerId); if (cacheInfo.getFetchStatus() == FetchStatus.success || !fetchedData.containsKey(accountId)) { fetchedData.put(accountId, fetchContext); @@ -277,23 +278,24 @@ private Future recoverFromFailedFetching(Throwable throwable, metrics.updatePriceFloorFetchMetric(MetricName.failure); final FetchStatus fetchStatus; + final String errorMessage; if (throwable instanceof TimeoutException || throwable instanceof ConnectTimeoutException) { fetchStatus = FetchStatus.timeout; - logger.error("Fetch price floor request timeout for fetch.url: '%s', account %s exceeded." - .formatted(fetchUrl, accountId)); + errorMessage = "Fetch price floor request timeout for fetch.url '%s' exceeded.".formatted(fetchUrl); } else { fetchStatus = FetchStatus.error; - logger.error( - "Failed to fetch price floor from provider for fetch.url: '%s', account = %s with a reason : %s " - .formatted(fetchUrl, accountId, throwable.getMessage())); + errorMessage = "Failed to fetch price floor from provider for fetch.url '%s', with a reason: %s" + .formatted(fetchUrl, throwable.getMessage()); } - return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus)); + logger.error("Price floor fetching failed for account %s: %s".formatted(accountId, errorMessage)); + return Future.succeededFuture(ResponseCacheInfo.withError(fetchStatus, errorMessage)); } private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData, AccountPriceFloorsFetchConfig fetchConfig, String accountId) { + final long accountPeriodicTimeSec = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec); final long periodicTimeSec = @@ -310,11 +312,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") @@ -324,6 +323,8 @@ private static class AccountFetchContext { FetchStatus fetchStatus; + String errorMessage; + Long maxAgeTimerId; } @@ -334,10 +335,12 @@ private static class ResponseCacheInfo { FetchStatus fetchStatus; + String errorMessage; + Long cacheTtl; - public static ResponseCacheInfo withStatus(FetchStatus status) { - return ResponseCacheInfo.of(null, status, null); + public static ResponseCacheInfo withError(FetchStatus status, String errorMessage) { + return ResponseCacheInfo.of(null, status, errorMessage, null); } } } 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/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java index b976ea69c97..028686f82d4 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java @@ -4,12 +4,16 @@ import org.apache.commons.collections4.MapUtils; import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorData; +import org.prebid.server.floors.model.PriceFloorField; import org.prebid.server.floors.model.PriceFloorModelGroup; import org.prebid.server.floors.model.PriceFloorRules; +import org.prebid.server.floors.model.PriceFloorSchema; import java.math.BigDecimal; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; public class PriceFloorRulesValidator { @@ -17,11 +21,13 @@ public class PriceFloorRulesValidator { private static final int MODEL_WEIGHT_MIN_VALUE = 1; private static final int SKIP_RATE_MIN = 0; private static final int SKIP_RATE_MAX = 100; + private static final int USE_FETCH_DATA_RATE_MIN = 0; + private static final int USE_FETCH_DATA_RATE_MAX = 100; private PriceFloorRulesValidator() { } - public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) { + public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules, Integer maxDimensions) { final Integer rootSkipRate = priceFloorRules.getSkipRate(); if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) { @@ -34,10 +40,10 @@ public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRul throw new PreBidException("Price floor floorMin must be positive float, but was " + floorMin); } - validateRulesData(priceFloorRules.getData(), maxRules); + validateRulesData(priceFloorRules.getData(), maxRules, maxDimensions); } - public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) { + public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules, Integer maxDimensions) { if (priceFloorData == null) { throw new PreBidException("Price floor rules data must be present"); } @@ -48,16 +54,24 @@ public static void validateRulesData(PriceFloorData priceFloorData, Integer maxR "Price floor data skipRate must be in range(0-100), but was " + dataSkipRate); } + final Integer useFetchDataRate = priceFloorData.getUseFetchDataRate(); + if (useFetchDataRate != null + && (useFetchDataRate < USE_FETCH_DATA_RATE_MIN || useFetchDataRate > USE_FETCH_DATA_RATE_MAX)) { + + throw new PreBidException( + "Price floor data useFetchDataRate must be in range(0-100), but was " + useFetchDataRate); + } + if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) { throw new PreBidException("Price floor rules should contain at least one model group"); } priceFloorData.getModelGroups().stream() .filter(Objects::nonNull) - .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules)); + .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules, maxDimensions)); } - private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) { + private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules, Integer maxDimensions) { final Integer modelWeight = modelGroup.getModelWeight(); if (modelWeight != null && (modelWeight < MODEL_WEIGHT_MIN_VALUE || modelWeight > MODEL_WEIGHT_MAX_VALUE)) { @@ -85,8 +99,21 @@ private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer } if (maxRules != null && values.size() > maxRules) { - throw new PreBidException( - "Price floor rules number %s exceeded its maximum number %s".formatted(values.size(), maxRules)); + throw new PreBidException("Price floor rules number %s exceeded its maximum number %s" + .formatted(values.size(), maxRules)); + } + + final List fields = Optional.ofNullable(modelGroup.getSchema()) + .map(PriceFloorSchema::getFields) + .orElse(null); + + if (CollectionUtils.isEmpty(fields)) { + throw new PreBidException("Price floor dimensions can't be null or empty, but were " + fields); + } + + if (maxDimensions != null && fields.size() > maxDimensions) { + throw new PreBidException("Price floor schema dimensions %s exceeded its maximum number %s" + .formatted(fields.size(), maxDimensions)); } } } diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java index d5bbde4b476..14834e24b7c 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; @@ -23,38 +23,30 @@ public class PriceFloorsConfigResolver { private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); private static final int MIN_MAX_AGE_SEC_VALUE = 600; + private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE; private static final int MIN_PERIODIC_SEC_VALUE = 300; private static final int MIN_TIMEOUT_MS_VALUE = 10; private static final int MAX_TIMEOUT_MS_VALUE = 10_000; private static final int MIN_RULES_VALUE = 0; - private static final int MIN_FILE_SIZE_VALUE = 0; - private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE; private static final int MAX_RULES_VALUE = Integer.MAX_VALUE; + private static final int MIN_DIMENSIONS_VALUE = 0; + private static final int MAX_DIMENSIONS_VALUE = 19; + private static final int MIN_FILE_SIZE_VALUE = 0; private static final int MAX_FILE_SIZE_VALUE = Integer.MAX_VALUE; private static final int MIN_ENFORCE_RATE_VALUE = 0; 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 +57,75 @@ 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 Long maxRules = floorsConfig.getMaxRules(); + if (maxRules != null && isNotInRange(maxRules, MIN_RULES_VALUE, MAX_RULES_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules)); + } + + final Long maxDimensions = floorsConfig.getMaxSchemaDims(); + if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); + } + 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 maxDimensions = fetchConfig.getMaxSchemaDims(); + if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) { + throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions)); + } + + 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)); } @@ -147,13 +139,7 @@ private static String invalidPriceFloorsPropertyMessage(String property, Object 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(); + public static int resolveMaxValue(Long value) { + return value != null && !value.equals(0L) ? Math.toIntExact(value) : Integer.MAX_VALUE; } } 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..4604ed91892 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java @@ -18,6 +18,9 @@ public class PriceFloorData { @JsonProperty("skipRate") Integer skipRate; + @JsonProperty("useFetchDataRate") + Integer useFetchDataRate; + @JsonProperty("floorsSchemaVersion") String floorsSchemaVersion; @@ -26,4 +29,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/floors/model/PriceFloorResult.java b/src/main/java/org/prebid/server/floors/model/PriceFloorResult.java index fdb9ff44c97..f0c498a095b 100644 --- a/src/main/java/org/prebid/server/floors/model/PriceFloorResult.java +++ b/src/main/java/org/prebid/server/floors/model/PriceFloorResult.java @@ -14,8 +14,4 @@ public class PriceFloorResult { BigDecimal floorValue; String currency; - - public static PriceFloorResult empty() { - return PriceFloorResult.of(null, null, null, null); - } } diff --git a/src/main/java/org/prebid/server/floors/proto/FetchResult.java b/src/main/java/org/prebid/server/floors/proto/FetchResult.java index 36c4fda58e0..336bb26ee43 100644 --- a/src/main/java/org/prebid/server/floors/proto/FetchResult.java +++ b/src/main/java/org/prebid/server/floors/proto/FetchResult.java @@ -9,4 +9,18 @@ public class FetchResult { PriceFloorData rulesData; FetchStatus fetchStatus; + + String errorMessage; + + public static FetchResult none(String errorMessage) { + return FetchResult.of(null, FetchStatus.none, errorMessage); + } + + public static FetchResult error(String errorMessage) { + return FetchResult.of(null, FetchStatus.error, errorMessage); + } + + public static FetchResult inProgress() { + return FetchResult.of(null, FetchStatus.inprogress, null); + } } diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java index f7a2c6ca6b9..f0fc0be3574 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.execution.timeout.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; @@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit; /** - * Wrapper for geo location service with circuit breaker. + * Wrapper for geolocation service with circuit breaker. */ public class CircuitBreakerSecuredGeoLocationService implements GeoLocationService { diff --git a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java index 72ec6feb2b2..30d78ea27c0 100644 --- a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.geolocation.model.GeoInfoConfiguration; diff --git a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java index 7604a25c71a..3d4c582db38 100644 --- a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java @@ -1,7 +1,7 @@ package org.prebid.server.geolocation; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; /** diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java index b258f14a531..8f883a3c393 100644 --- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java +++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java @@ -14,8 +14,8 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.RemoteFileProcessor; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.file.FileProcessor; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.geolocation.model.GeoInfo; import java.io.FileInputStream; @@ -28,7 +28,7 @@ * Implementation of the {@link GeoLocationService} * backed by MaxMind free database */ -public class MaxMindGeoLocationService implements GeoLocationService, RemoteFileProcessor { +public class MaxMindGeoLocationService implements GeoLocationService, FileProcessor { private static final String VENDOR = "maxmind"; @@ -42,7 +42,7 @@ public Future setDataPath(String dataFilePath) { TarArchiveEntry currentEntry; boolean hasDatabaseFile = false; - while ((currentEntry = tarInput.getNextTarEntry()) != null) { + while ((currentEntry = tarInput.getNextEntry()) != null) { if (currentEntry.getName().contains(DATABASE_FILE_NAME)) { hasDatabaseFile = true; break; @@ -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..cf332416092 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; @@ -27,11 +24,13 @@ import org.prebid.server.cookie.model.CookieSyncContext; import org.prebid.server.cookie.model.PartitionedCookie; import org.prebid.server.exception.InvalidAccountConfigException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; 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,15 +40,19 @@ 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); + private static final ConditionalLogger badRequestLogger = new ConditionalLogger(logger); private final long defaultTimeout; private final double logSamplingRate; @@ -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(); @@ -138,7 +146,7 @@ private Future cookieSyncContext(RoutingContext routingContex } private CookieSyncRequest parseRequest(RoutingContext routingContext) { - final Buffer body = routingContext.getBody(); + final Buffer body = routingContext.body().buffer(); if (body == null) { throw new InvalidCookieSyncRequestException("Request has no body"); } @@ -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(); + badRequestLogger.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; + + badRequestLogger.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..1bff922f5c3 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(); @@ -38,7 +38,7 @@ private static boolean shouldLogException(Throwable exception) { private static boolean isConnectionResetException(Throwable exception) { return exception instanceof IOException - && StringUtils.equals("readAddress(..) failed: Connection reset by peer", exception.getMessage()); + && StringUtils.equals("recvAddress(..) failed: Connection reset by peer", exception.getMessage()); } private static String errorMessageFrom(Throwable exception) { 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/GetVtrackHandler.java b/src/main/java/org/prebid/server/handler/GetVtrackHandler.java new file mode 100644 index 00000000000..e70e26627fc --- /dev/null +++ b/src/main/java/org/prebid/server/handler/GetVtrackHandler.java @@ -0,0 +1,108 @@ +package org.prebid.server.handler; + +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.AsyncResult; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.cache.CoreCacheService; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.model.Endpoint; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; +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 GetVtrackHandler implements ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(GetVtrackHandler.class); + + private static final String UUID_PARAMETER = "uuid"; + private static final String CH_PARAMETER = "ch"; + + private final long defaultTimeout; + private final CoreCacheService coreCacheService; + private final TimeoutFactory timeoutFactory; + + public GetVtrackHandler(long defaultTimeout, CoreCacheService coreCacheService, TimeoutFactory timeoutFactory) { + this.defaultTimeout = defaultTimeout; + this.coreCacheService = Objects.requireNonNull(coreCacheService); + this.timeoutFactory = Objects.requireNonNull(timeoutFactory); + } + + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.vtrack.value())); + } + + @Override + public void handle(RoutingContext routingContext) { + final String uuid = routingContext.request().getParam(UUID_PARAMETER); + final String ch = routingContext.request().getParam(CH_PARAMETER); + if (StringUtils.isBlank(uuid)) { + respondWith( + routingContext, + HttpResponseStatus.BAD_REQUEST, + "'%s' is a required query parameter and can't be empty".formatted(UUID_PARAMETER)); + return; + } + + final Timeout timeout = timeoutFactory.create(defaultTimeout); + + coreCacheService.getCachedObject(uuid, ch, timeout) + .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); + } + + private static void respondWithServerError(RoutingContext routingContext, Throwable exception) { + logger.error("Error occurred while sending request to cache", exception); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, + "%s: %s".formatted("Error occurred while sending request to cache", exception.getMessage())); + } + + private static void respondWith(RoutingContext routingContext, + HttpResponseStatus status, + MultiMap headers, + String body) { + + HttpUtil.executeSafely( + routingContext, + Endpoint.vtrack, + response -> { + headers.forEach(response::putHeader); + response.setStatusCode(status.code()) .end(body); + }); + } + + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely( + routingContext, + Endpoint.vtrack, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .setStatusCode(status.code()) + .end(body)); + } + + private void handleCacheResult(AsyncResult async, RoutingContext routingContext) { + if (async.failed()) { + respondWithServerError(routingContext, async.cause()); + } else { + final HttpClientResponse response = async.result(); + final HttpResponseStatus status = HttpResponseStatus.valueOf(response.getStatusCode()); + if (status == HttpResponseStatus.OK) { + respondWith(routingContext, status, response.getHeaders(), response.getBody()); + } else { + respondWith(routingContext, status, response.getBody()); + } + } + } +} diff --git a/src/main/java/org/prebid/server/handler/GetuidsHandler.java b/src/main/java/org/prebid/server/handler/GetuidsHandler.java index deb3822e4a9..ec6f15e127b 100644 --- a/src/main/java/org/prebid/server/handler/GetuidsHandler.java +++ b/src/main/java/org/prebid/server/handler/GetuidsHandler.java @@ -1,21 +1,24 @@ 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; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.cookie.UidsCookieService; 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 +28,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); @@ -43,8 +51,7 @@ private Map uidsFrom(RoutingContext routingContext) { uidEntry -> uidEntry.getValue().getUid())); } - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class BuyerUids { @JsonInclude(JsonInclude.Include.NON_EMPTY) 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..60e11195c26 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.execution.timeout.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..5d9885c70ec 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); @@ -96,7 +106,8 @@ private Cookie optCookie(boolean optout, RoutingContext routingContext) { final UidsCookie uidsCookie = uidsCookieService .parseFromRequest(routingContext) .updateOptout(optout); - return uidsCookieService.toCookie(uidsCookie); + + return uidsCookieService.aliveCookie(uidsCookie); } private String optUrl(boolean optout) { diff --git a/src/main/java/org/prebid/server/handler/PostVtrackHandler.java b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java new file mode 100644 index 00000000000..c52068427ce --- /dev/null +++ b/src/main/java/org/prebid/server/handler/PostVtrackHandler.java @@ -0,0 +1,252 @@ +package org.prebid.server.handler; + +import com.fasterxml.jackson.databind.JsonNode; +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.buffer.Buffer; +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.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.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +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.settings.model.AccountVtrackConfig; +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.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class PostVtrackHandler implements ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(PostVtrackHandler.class); + + private static final String ACCOUNT_PARAMETER = "a"; + private static final String INTEGRATION_PARAMETER = "int"; + private static final String TYPE_XML = "xml"; + + private final long defaultTimeout; + private final boolean allowUnknownBidder; + private final boolean modifyVastForUnknownBidder; + private final ApplicationSettings applicationSettings; + private final BidderCatalog bidderCatalog; + private final CoreCacheService coreCacheService; + private final TimeoutFactory timeoutFactory; + private final JacksonMapper mapper; + + public PostVtrackHandler(long defaultTimeout, + boolean allowUnknownBidder, + boolean modifyVastForUnknownBidder, + ApplicationSettings applicationSettings, + BidderCatalog bidderCatalog, + CoreCacheService coreCacheService, + TimeoutFactory timeoutFactory, + JacksonMapper mapper) { + + this.defaultTimeout = defaultTimeout; + this.allowUnknownBidder = allowUnknownBidder; + this.modifyVastForUnknownBidder = modifyVastForUnknownBidder; + this.applicationSettings = Objects.requireNonNull(applicationSettings); + this.bidderCatalog = Objects.requireNonNull(bidderCatalog); + 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 String integration; + try { + accountId = accountId(routingContext); + vtrackPuts = vtrackPuts(routingContext); + integration = integration(routingContext); + } catch (IllegalArgumentException e) { + respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); + return; + } + final Timeout timeout = timeoutFactory.create(defaultTimeout); + + applicationSettings.getAccountById(accountId, timeout) + .recover(exception -> handleAccountExceptionOrFallback(exception, accountId)) + .onComplete(async -> handleAccountResult(async, routingContext, vtrackPuts, accountId, integration, + timeout)); + } + + private static String accountId(RoutingContext routingContext) { + final String accountId = routingContext.request().getParam(ACCOUNT_PARAMETER); + if (StringUtils.isEmpty(accountId)) { + throw new IllegalArgumentException( + "Account '%s' is required query parameter and can't be empty".formatted(ACCOUNT_PARAMETER)); + } + return accountId; + } + + private List vtrackPuts(RoutingContext routingContext) { + final Buffer body = routingContext.body().buffer(); + if (body == null || body.length() == 0) { + throw new IllegalArgumentException("Incoming request has no body"); + } + + final BidCacheRequest bidCacheRequest; + try { + bidCacheRequest = mapper.decodeValue(body, BidCacheRequest.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Failed to parse request body", e); + } + + final List bidPutObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); + for (BidPutObject bidPutObject : bidPutObjects) { + validatePutObject(bidPutObject); + } + return bidPutObjects; + } + + 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(bidPutObject.getBidder())) { + throw new IllegalArgumentException("'bidder' is required field and can't be empty"); + } + + if (!StringUtils.equals(bidPutObject.getType(), TYPE_XML)) { + throw new IllegalArgumentException("vtrack only accepts type xml"); + } + + final JsonNode value = bidPutObject.getValue(); + final String valueAsString = value != null ? value.asText() : null; + if (!StringUtils.containsIgnoreCase(valueAsString, " handleAccountExceptionOrFallback(Throwable exception, String accountId) { + return exception instanceof PreBidException + ? Future.succeededFuture(Account.builder() + .id(accountId) + .auction(AccountAuctionConfig.builder() + .events(AccountEventsConfig.of(false)) + .build()) + .build()) + : Future.failedFuture(exception); + } + + private void handleAccountResult(AsyncResult asyncAccount, + RoutingContext routingContext, + List vtrackPuts, + String accountId, + String integration, + Timeout timeout) { + + if (asyncAccount.failed()) { + respondWithServerError(routingContext, "Error occurred while fetching account", asyncAccount.cause()); + } else { + // insert impression tracking if account allows events and bidder allows VAST modification + final Account account = asyncAccount.result(); + final Boolean isEventEnabled = accountEventsEnabled(account); + final Integer accountTtl = accountVtrackTtl(account); + final Set allowedBidders = biddersAllowingVastUpdate(vtrackPuts); + coreCacheService.cachePutObjects( + vtrackPuts, isEventEnabled, allowedBidders, accountId, accountTtl, integration, timeout) + .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); + } + } + + private static Boolean accountEventsEnabled(Account account) { + final AccountAuctionConfig accountAuctionConfig = account.getAuction(); + final AccountEventsConfig accountEventsConfig = + accountAuctionConfig != null ? accountAuctionConfig.getEvents() : null; + + return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; + } + + private static Integer accountVtrackTtl(Account account) { + return Optional.ofNullable(account.getVtrack()) + .map(AccountVtrackConfig::getTtl) + .orElse(null); + } + + /** + * Returns list of bidders that allow VAST XML modification. + */ + private Set biddersAllowingVastUpdate(List vtrackPuts) { + return vtrackPuts.stream() + .map(BidPutObject::getBidder) + .filter(this::isAllowVastForBidder) + .collect(Collectors.toSet()); + } + + private boolean isAllowVastForBidder(String bidderName) { + if (bidderCatalog.isValidName(bidderName)) { + return bidderCatalog.isModifyingVastXmlAllowed(bidderName); + } else { + return allowUnknownBidder && modifyVastForUnknownBidder; + } + } + + private void handleCacheResult(AsyncResult async, RoutingContext routingContext) { + if (async.failed()) { + respondWithServerError(routingContext, "Error occurred while sending request to cache", async.cause()); + } else { + try { + respondWith(routingContext, HttpResponseStatus.OK, mapper.encodeToString(async.result())); + } catch (EncodeException e) { + respondWithServerError(routingContext, "Error occurred while encoding response", e); + } + } + } + + private static void respondWithServerError(RoutingContext routingContext, String message, Throwable exception) { + logger.error(message, exception); + respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, + "%s: %s".formatted(message, exception.getMessage())); + } + + private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { + HttpUtil.executeSafely(routingContext, Endpoint.vtrack, + response -> response + .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) + .setStatusCode(status.code()) + .end(body)); + } +} diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java index 5472e5a36b1..bce568db2d7 100644 --- a/src/main/java/org/prebid/server/handler/SetuidHandler.java +++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java @@ -2,18 +2,18 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.AsyncResult; +import io.vertx.core.CompositeFuture; 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; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -28,7 +28,6 @@ import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.UsersyncFormat; -import org.prebid.server.bidder.UsersyncMethod; import org.prebid.server.bidder.UsersyncMethodType; import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; @@ -36,13 +35,15 @@ import org.prebid.server.cookie.UidsCookieService; import org.prebid.server.cookie.exception.UnauthorizedUidsException; import org.prebid.server.cookie.exception.UnavailableForLegalReasonsException; -import org.prebid.server.cookie.model.UidsCookieUpdateResult; import org.prebid.server.exception.InvalidAccountConfigException; import org.prebid.server.exception.InvalidRequestException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.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.model.UpdateResult; import org.prebid.server.privacy.HostVendorTcfDefinerService; import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse; import org.prebid.server.privacy.gdpr.model.PrivacyEnforcementAction; @@ -50,20 +51,25 @@ import org.prebid.server.privacy.gdpr.model.TcfResponse; import org.prebid.server.settings.ApplicationSettings; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountGdprConfig; +import org.prebid.server.settings.model.AccountPrivacyConfig; import org.prebid.server.util.HttpUtil; +import org.prebid.server.util.StreamUtil; +import org.prebid.server.vertx.verticles.server.HttpEndpoint; +import org.prebid.server.vertx.verticles.server.application.ApplicationResource; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; +import java.util.function.Predicate; 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); @@ -83,7 +89,7 @@ public class SetuidHandler implements Handler { private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; private final TimeoutFactory timeoutFactory; - private final Map cookieNameToSyncType; + private final Map> cookieNameToBidderAndSyncType; public SetuidHandler(long defaultTimeout, UidsCookieService uidsCookieService, @@ -107,47 +113,58 @@ public SetuidHandler(long defaultTimeout, this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.cookieNameToSyncType = collectMap(bidderCatalog); + this.cookieNameToBidderAndSyncType = collectUsersyncers(bidderCatalog); } - private static Map collectMap(BidderCatalog bidderCatalog) { + private static Map> collectUsersyncers(BidderCatalog bidderCatalog) { + validateUsersyncersDuplicates(bidderCatalog); + + return bidderCatalog.usersyncReadyBidders().stream() + .sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName)))) + .filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName)) + .map(bidderName -> bidderCatalog.usersyncerByName(bidderName) + .map(usersyncer -> Pair.of(bidderName, usersyncer))) + .flatMap(Optional::stream) + .collect(Collectors.toMap( + pair -> pair.getRight().getCookieFamilyName(), + pair -> Pair.of(pair.getLeft(), preferredUserSyncType(pair.getRight())))); + } - final Supplier> usersyncers = () -> bidderCatalog.names() - .stream() - .filter(bidderCatalog::isActive) + private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) { + final List duplicatedCookieFamilyNames = bidderCatalog.usersyncReadyBidders().stream() + .filter(bidderName -> !isAliasWithRootCookieFamilyName(bidderCatalog, bidderName)) .map(bidderCatalog::usersyncerByName) - .filter(Optional::isPresent) - .map(Optional::get) - .distinct(); + .flatMap(Optional::stream) + .map(Usersyncer::getCookieFamilyName) + .filter(Predicate.not(StreamUtil.distinctBy(Function.identity()))) + .distinct() + .sorted() + .toList(); + + if (!duplicatedCookieFamilyNames.isEmpty()) { + throw new IllegalArgumentException( + "Duplicated \"cookie-family-name\" found, values: " + + String.join(", ", duplicatedCookieFamilyNames)); + } + } - validateUsersyncers(usersyncers.get()); + private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatalog, String bidder) { + final String bidderCookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElse(StringUtils.EMPTY); + final String parentCookieFamilyName = + bidderCatalog.cookieFamilyName(bidderCatalog.resolveBaseBidder(bidder)).orElse(null); - return usersyncers.get() - .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType)); + return bidderCatalog.isAlias(bidder) + && parentCookieFamilyName != null + && parentCookieFamilyName.equals(bidderCookieFamilyName); } private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) { - return Stream.of(usersyncer.getIframe(), usersyncer.getRedirect()) - .filter(Objects::nonNull) - .findFirst() - .map(UsersyncMethod::getType) - .get(); // when usersyncer is present, it will contain at least one method + return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType(); } - private static void validateUsersyncers(Stream usersyncers) { - final List cookieFamilyNameDuplicates = usersyncers.map(Usersyncer::getCookieFamilyName) - .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) - .entrySet() - .stream() - .filter(name -> name.getValue() > 1) - .map(Map.Entry::getKey) - .distinct() - .toList(); - if (CollectionUtils.isNotEmpty(cookieFamilyNameDuplicates)) { - throw new IllegalArgumentException( - "Duplicated \"cookie-family-name\" found, values: " - + String.join(", ", cookieFamilyNameDuplicates)); - } + @Override + public List endpoints() { + return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value())); } @Override @@ -163,6 +180,11 @@ private Future toSetuidContext(RoutingContext routingContext) { final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM); final Timeout timeout = timeoutFactory.create(defaultTimeout); + final UsersyncMethodType syncType = Optional.ofNullable(cookieName) + .map(cookieNameToBidderAndSyncType::get) + .map(Pair::getRight) + .orElse(null); + return accountById(requestAccount, timeout) .compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout) .map(privacyContext -> SetuidContext.builder() @@ -171,7 +193,7 @@ private Future toSetuidContext(RoutingContext routingContext) { .timeout(timeout) .account(account) .cookieName(cookieName) - .syncType(cookieNameToSyncType.get(cookieName)) + .syncType(syncType) .privacyContext(privacyContext) .build())) @@ -184,10 +206,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) { @@ -204,35 +223,46 @@ private void handleSetuidContextResult(AsyncResult setuidContextR if (setuidContextResult.succeeded()) { final SetuidContext setuidContext = setuidContextResult.result(); - final String bidder = setuidContext.getCookieName(); + final String bidderCookieFamily = setuidContext.getCookieName(); final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); try { - validateSetuidContext(setuidContext, bidder); + validateSetuidContext(setuidContext, bidderCookieFamily); } catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) { handleErrors(e, routingContext, tcfContext); return; } - isAllowedForHostVendorId(tcfContext) - .onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext)); + final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy(); + final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null; + + final String bidderName = cookieNameToBidderAndSyncType.get(bidderCookieFamily).getLeft(); + + Future.all( + tcfDefinerService.isAllowedForHostVendorId(tcfContext), + tcfDefinerService.resultForBidderNames( + Collections.singleton(bidderName), tcfContext, accountGdprConfig)) + .onComplete(hostTcfResponseResult -> respondByTcfResponse( + hostTcfResponseResult, + bidderName, + setuidContext)); } else { final Throwable error = setuidContextResult.cause(); handleErrors(error, routingContext, null); } } - private void validateSetuidContext(SetuidContext setuidContext, String bidder) { + private void validateSetuidContext(SetuidContext setuidContext, String bidderCookieFamily) { final String cookieName = setuidContext.getCookieName(); final boolean isCookieNameBlank = StringUtils.isBlank(cookieName); - if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) { + if (isCookieNameBlank || !cookieNameToBidderAndSyncType.containsKey(cookieName)) { final String cookieNameError = isCookieNameBlank ? "required" : "invalid"; throw new InvalidRequestException("\"bidder\" query param is " + cookieNameError); } final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); if (tcfContext.isInGdprScope() && !tcfContext.isConsentValid()) { - metrics.updateUserSyncTcfInvalidMetric(bidder); + metrics.updateUserSyncTcfInvalidMetric(bidderCookieFamily); throw new InvalidRequestException("Consent string is invalid"); } @@ -243,7 +273,7 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) { final ActivityInfrastructure activityInfrastructure = setuidContext.getActivityInfrastructure(); final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload.of( - ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidder), + ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidderCookieFamily), tcfContext); if (!activityInfrastructure.isAllowed(Activity.SYNC_USER, activityInvocationPayload)) { @@ -251,47 +281,30 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) { } } - /** - * If host vendor id is null, host allowed to setuid. - */ - private Future isAllowedForHostVendorId(TcfContext tcfContext) { - final Integer gdprHostVendorId = tcfDefinerService.getGdprHostVendorId(); - return gdprHostVendorId == null - ? Future.succeededFuture(HostVendorTcfResponse.allowedVendor()) - : tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext) - .map(this::toHostVendorTcfResponse); - } - - private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse tcfResponse) { - return HostVendorTcfResponse.of(tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(), - isSetuidAllowed(tcfResponse)); - } - - private boolean isSetuidAllowed(TcfResponse hostTcfResponseToSetuidContext) { - // allow cookie only if user is not in GDPR scope or vendor passed GDPR check - final boolean notInGdprScope = BooleanUtils.isFalse(hostTcfResponseToSetuidContext.getUserInGdprScope()); - - final Map vendorIdToAction = hostTcfResponseToSetuidContext.getActions(); - final PrivacyEnforcementAction hostPrivacyAction = vendorIdToAction != null - ? vendorIdToAction.get(tcfDefinerService.getGdprHostVendorId()) - : null; - final boolean blockPixelSync = hostPrivacyAction == null || hostPrivacyAction.isBlockPixelSync(); - - return notInGdprScope || !blockPixelSync; - } - - private void respondByTcfResponse(AsyncResult hostTcfResponseResult, + private void respondByTcfResponse(AsyncResult hostTcfResponseResult, + String bidderName, SetuidContext setuidContext) { - final String bidderCookieName = setuidContext.getCookieName(); + final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext(); final RoutingContext routingContext = setuidContext.getRoutingContext(); if (hostTcfResponseResult.succeeded()) { - final HostVendorTcfResponse hostTcfResponse = hostTcfResponseResult.result(); - if (hostTcfResponse.isVendorAllowed()) { + final CompositeFuture compositeFuture = hostTcfResponseResult.result(); + final HostVendorTcfResponse hostVendorTcfResponse = compositeFuture.resultAt(0); + final TcfResponse bidderTcfResponse = compositeFuture.resultAt(1); + + final Map vendorIdToAction = bidderTcfResponse.getActions(); + final PrivacyEnforcementAction action = vendorIdToAction != null + ? vendorIdToAction.get(bidderName) + : null; + + final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope()); + final boolean isBidderVendorAllowed = notInGdprScope || action == null || !action.isBlockPixelSync(); + + if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) { respondWithCookie(setuidContext); } else { - metrics.updateUserSyncTcfBlockedMetric(bidderCookieName); + metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName()); final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable for legal reasons"); @@ -304,10 +317,9 @@ private void respondByTcfResponse(AsyncResult hostTcfResp analyticsDelegator.processEvent(SetuidEvent.error(status.code()), tcfContext); } - } else { final Throwable error = hostTcfResponseResult.cause(); - metrics.updateUserSyncTcfBlockedMetric(bidderCookieName); + metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName()); handleErrors(error, routingContext, tcfContext); } } @@ -317,12 +329,13 @@ private void respondWithCookie(SetuidContext setuidContext) { final String uid = routingContext.request().getParam(UID_PARAM); final String bidder = setuidContext.getCookieName(); - final UidsCookieUpdateResult uidsCookieUpdateResult = - uidsCookieService.updateUidsCookie(setuidContext.getUidsCookie(), bidder, uid); - final Cookie updatedUidsCookie = uidsCookieService.toCookie(uidsCookieUpdateResult.getUidsCookie()); - addCookie(routingContext, updatedUidsCookie); + final UpdateResult uidsCookieUpdateResult = uidsCookieService.updateUidsCookie( + setuidContext.getUidsCookie(), bidder, uid); + + uidsCookieService.splitUidsIntoCookies(uidsCookieUpdateResult.getValue()) + .forEach(cookie -> addCookie(routingContext, cookie)); - if (uidsCookieUpdateResult.isSuccessfullyUpdated()) { + if (uidsCookieUpdateResult.isUpdated()) { metrics.updateUserSyncSetsMetric(bidder); } final int statusCode = HttpResponseStatus.OK.code(); @@ -333,7 +346,7 @@ private void respondWithCookie(SetuidContext setuidContext) { .status(statusCode) .bidder(bidder) .uid(uid) - .success(uidsCookieUpdateResult.isSuccessfullyUpdated()) + .success(uidsCookieUpdateResult.isUpdated()) .build(); analyticsDelegator.processEvent(setuidEvent, tcfContext); } @@ -360,25 +373,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 deleted file mode 100644 index d433867b01e..00000000000 --- a/src/main/java/org/prebid/server/handler/VtrackHandler.java +++ /dev/null @@ -1,233 +0,0 @@ -package org.prebid.server.handler; - -import com.fasterxml.jackson.databind.JsonNode; -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.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -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.events.EventUtil; -import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.EncodeException; -import org.prebid.server.json.JacksonMapper; -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 java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -public class VtrackHandler implements Handler { - - private static final Logger logger = LoggerFactory.getLogger(VtrackHandler.class); - - private static final String ACCOUNT_PARAMETER = "a"; - private static final String INTEGRATION_PARAMETER = "int"; - private static final String TYPE_XML = "xml"; - - private final long defaultTimeout; - private final boolean allowUnknownBidder; - private final boolean modifyVastForUnknownBidder; - private final ApplicationSettings applicationSettings; - private final BidderCatalog bidderCatalog; - private final CacheService cacheService; - private final TimeoutFactory timeoutFactory; - private final JacksonMapper mapper; - - public VtrackHandler(long defaultTimeout, - boolean allowUnknownBidder, - boolean modifyVastForUnknownBidder, - ApplicationSettings applicationSettings, - BidderCatalog bidderCatalog, - CacheService cacheService, - TimeoutFactory timeoutFactory, - JacksonMapper mapper) { - - this.defaultTimeout = defaultTimeout; - this.allowUnknownBidder = allowUnknownBidder; - this.modifyVastForUnknownBidder = modifyVastForUnknownBidder; - this.applicationSettings = Objects.requireNonNull(applicationSettings); - this.bidderCatalog = Objects.requireNonNull(bidderCatalog); - this.cacheService = Objects.requireNonNull(cacheService); - this.timeoutFactory = Objects.requireNonNull(timeoutFactory); - this.mapper = Objects.requireNonNull(mapper); - } - - @Override - public void handle(RoutingContext routingContext) { - final String accountId; - final List vtrackPuts; - final String integration; - try { - accountId = accountId(routingContext); - vtrackPuts = vtrackPuts(routingContext); - integration = integration(routingContext); - } catch (IllegalArgumentException e) { - respondWith(routingContext, HttpResponseStatus.BAD_REQUEST, e.getMessage()); - return; - } - final Timeout timeout = timeoutFactory.create(defaultTimeout); - - applicationSettings.getAccountById(accountId, timeout) - .recover(exception -> handleAccountExceptionOrFallback(exception, accountId)) - .onComplete(async -> handleAccountResult(async, routingContext, vtrackPuts, accountId, integration, - timeout)); - } - - private static String accountId(RoutingContext routingContext) { - final String accountId = routingContext.request().getParam(ACCOUNT_PARAMETER); - if (StringUtils.isEmpty(accountId)) { - throw new IllegalArgumentException( - "Account '%s' is required query parameter and can't be empty".formatted(ACCOUNT_PARAMETER)); - } - return accountId; - } - - private List vtrackPuts(RoutingContext routingContext) { - final Buffer body = routingContext.getBody(); - if (body == null || body.length() == 0) { - throw new IllegalArgumentException("Incoming request has no body"); - } - - final BidCacheRequest bidCacheRequest; - try { - bidCacheRequest = mapper.decodeValue(body, BidCacheRequest.class); - } catch (DecodeException e) { - throw new IllegalArgumentException("Failed to parse request body", e); - } - - final List putObjects = ListUtils.emptyIfNull(bidCacheRequest.getPuts()); - for (PutObject putObject : putObjects) { - validatePutObject(putObject); - } - return putObjects; - } - - private static void validatePutObject(PutObject putObject) { - if (StringUtils.isEmpty(putObject.getBidid())) { - throw new IllegalArgumentException("'bidid' is required field and can't be empty"); - } - - if (StringUtils.isEmpty(putObject.getBidder())) { - throw new IllegalArgumentException("'bidder' is required field and can't be empty"); - } - - if (!StringUtils.equals(putObject.getType(), TYPE_XML)) { - throw new IllegalArgumentException("vtrack only accepts type xml"); - } - - final JsonNode value = putObject.getValue(); - final String valueAsString = value != null ? value.asText() : null; - if (!StringUtils.containsIgnoreCase(valueAsString, " handleAccountExceptionOrFallback(Throwable exception, String accountId) { - return exception instanceof PreBidException - ? Future.succeededFuture(Account.builder() - .id(accountId) - .auction(AccountAuctionConfig.builder() - .events(AccountEventsConfig.of(false)) - .build()) - .build()) - : Future.failedFuture(exception); - } - - private void handleAccountResult(AsyncResult asyncAccount, - RoutingContext routingContext, - List vtrackPuts, - String accountId, - String integration, - Timeout timeout) { - - if (asyncAccount.failed()) { - respondWithServerError(routingContext, "Error occurred while fetching account", asyncAccount.cause()); - } else { - // 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) - .onComplete(asyncCache -> handleCacheResult(asyncCache, routingContext)); - } - } - - private static Boolean accountEventsEnabled(Account account) { - final AccountAuctionConfig accountAuctionConfig = account.getAuction(); - final AccountEventsConfig accountEventsConfig = - accountAuctionConfig != null ? accountAuctionConfig.getEvents() : null; - - return accountEventsConfig != null ? accountEventsConfig.getEnabled() : null; - } - - /** - * Returns list of bidders that allow VAST XML modification. - */ - private Set biddersAllowingVastUpdate(List vtrackPuts) { - return vtrackPuts.stream() - .map(PutObject::getBidder) - .filter(this::isAllowVastForBidder) - .collect(Collectors.toSet()); - } - - private boolean isAllowVastForBidder(String bidderName) { - if (bidderCatalog.isValidName(bidderName)) { - return bidderCatalog.isModifyingVastXmlAllowed(bidderName); - } else { - return allowUnknownBidder && modifyVastForUnknownBidder; - } - } - - private void handleCacheResult(AsyncResult async, RoutingContext routingContext) { - if (async.failed()) { - respondWithServerError(routingContext, "Error occurred while sending request to cache", async.cause()); - } else { - try { - respondWith(routingContext, HttpResponseStatus.OK, mapper.encodeToString(async.result())); - } catch (EncodeException e) { - respondWithServerError(routingContext, "Error occurred while encoding response", e); - } - } - } - - private static void respondWithServerError(RoutingContext routingContext, String message, Throwable exception) { - logger.error(message, exception); - respondWith(routingContext, HttpResponseStatus.INTERNAL_SERVER_ERROR, - "%s: %s".formatted(message, exception.getMessage())); - } - - private static void respondWith(RoutingContext routingContext, HttpResponseStatus status, String body) { - HttpUtil.executeSafely(routingContext, Endpoint.vtrack, - response -> response - .putHeader(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON) - .setStatusCode(status.code()) - .end(body)); - } -} 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 94% 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..277d2b792de 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,15 @@ -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; @@ -77,8 +76,7 @@ private void respondWith(RoutingContext routingContext, HttpResponseStatus statu .end(body)); } - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class Response { boolean active; 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 76% 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..fd652ebb8ff 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; @@ -18,23 +19,28 @@ */ public class SettingsCacheNotificationHandler implements Handler { - private final CacheNotificationListener cacheNotificationListener; - private final JacksonMapper mapper; private final String endpoint; + private final CacheNotificationListener cacheNotificationListener; + private final JacksonMapper mapper; + + public SettingsCacheNotificationHandler(String endpoint, + CacheNotificationListener cacheNotificationListener, + JacksonMapper mapper) { - public SettingsCacheNotificationHandler(CacheNotificationListener cacheNotificationListener, JacksonMapper mapper, - String endpoint) { + this.endpoint = Objects.requireNonNull(endpoint); this.cacheNotificationListener = Objects.requireNonNull(cacheNotificationListener); this.mapper = Objects.requireNonNull(mapper); - this.endpoint = Objects.requireNonNull(endpoint); } @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); } } @@ -42,7 +48,7 @@ public void handle(RoutingContext routingContext) { * Propagates updating settings cache. */ private void doSave(RoutingContext routingContext) { - final Buffer body = routingContext.getBody(); + final Buffer body = routingContext.body().buffer(); if (body == null) { respondWithBadRequest(routingContext, "Missing update data."); return; @@ -64,7 +70,7 @@ private void doSave(RoutingContext routingContext) { * Propagates invalidating settings cache. */ private void doInvalidate(RoutingContext routingContext) { - final Buffer body = routingContext.getBody(); + final Buffer body = routingContext.body().buffer(); if (body == null) { respondWithBadRequest(routingContext, "Missing invalidation data."); return; @@ -90,14 +96,18 @@ private void doFail(RoutingContext routingContext) { } private void respondWithBadRequest(RoutingContext routingContext, String body) { - HttpUtil.executeSafely(routingContext, endpoint, + HttpUtil.executeSafely( + routingContext, + endpoint, response -> response .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) .end(body)); } private void respondWith(RoutingContext routingContext, HttpResponseStatus status) { - HttpUtil.executeSafely(routingContext, endpoint, + HttpUtil.executeSafely( + routingContext, + endpoint, response -> response .setStatusCode(status.code()) .end()); 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 90% 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..a5382ac5b08 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,14 @@ -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; @@ -56,8 +55,7 @@ public void handle(RoutingContext routingContext) { } } - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class RevisionResponse { String revision; 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..a7b39dce659 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; @@ -24,21 +22,28 @@ import org.prebid.server.analytics.model.AmpEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; import org.prebid.server.auction.AmpResponsePostProcessor; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.Tuple2; 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; import org.prebid.server.exception.UnauthorizedAccountException; +import org.prebid.server.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; 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 +61,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 +74,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); @@ -79,12 +86,14 @@ public class AmpHandler implements Handler { private final ExchangeService exchangeService; private final AnalyticsReporterDelegator analyticsDelegator; private final Metrics metrics; + private final HooksMetricsService hooksMetricsService; private final Clock clock; private final BidderCatalog bidderCatalog; private final Set biddersSupportingCustomTargeting; private final AmpResponsePostProcessor ampResponsePostProcessor; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; private final double logSamplingRate; @@ -92,12 +101,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, ExchangeService exchangeService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, BidderCatalog bidderCatalog, Set biddersSupportingCustomTargeting, AmpResponsePostProcessor ampResponsePostProcessor, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper, double logSamplingRate) { @@ -105,16 +116,23 @@ public AmpHandler(AmpRequestFactory ampRequestFactory, this.exchangeService = Objects.requireNonNull(exchangeService); this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator); this.metrics = Objects.requireNonNull(metrics); + this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.bidderCatalog = Objects.requireNonNull(bidderCatalog); this.biddersSupportingCustomTargeting = Objects.requireNonNull(biddersSupportingCustomTargeting); this.ampResponsePostProcessor = Objects.requireNonNull(ampResponsePostProcessor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.mapper = Objects.requireNonNull(mapper); 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 @@ -127,18 +145,25 @@ public void handle(RoutingContext routingContext) { .httpContext(HttpRequestContext.from(routingContext)); ampRequestFactory.fromRequest(routingContext, startTime) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) .map(this::updateAppAndNoCookieAndImpsMetrics) - .compose(exchangeService::holdAuction) - .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), ampEventBuilder::bidResponse, context)) - .compose(context -> prepareAmpResponse(context, routingContext)) - .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(context -> addContextAndBidResponseToEvent(context, ampEventBuilder, context)) + .compose(context -> prepareSuccessfulResponse(context, routingContext, ampEventBuilder)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent(context.getAuctionContext(), ampEventBuilder, context)) .onComplete(responseResult -> handleResult(responseResult, ampEventBuilder, routingContext, startTime)); } + private static R addContextAndBidResponseToEvent(AuctionContext context, + AmpEvent.AmpEventBuilder ampEventBuilder, + R result) { + + ampEventBuilder.auctionContext(context); + ampEventBuilder.bidResponse(context.getBidResponse()); + return result; + } + private static R addToEvent(T field, Consumer consumer, R result) { consumer.accept(field); return result; @@ -159,8 +184,44 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } + private Future prepareSuccessfulResponse(AuctionContext auctionContext, + RoutingContext routingContext, + AmpEvent.AmpEventBuilder ampEventBuilder) { + + final String origin = originFrom(routingContext); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext, origin) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return prepareAmpResponse(auctionContext, routingContext) + .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result)) + .map(result -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(result.getLeft())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + private Future> prepareAmpResponse(AuctionContext context, RoutingContext routingContext) { + final BidRequest bidRequest = context.getBidRequest(); final BidResponse bidResponse = context.getBidResponse(); final AmpResponse ampResponse = toAmpResponse(bidResponse); @@ -264,12 +325,13 @@ private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) { : null; } - private void handleResult(AsyncResult> responseResult, + private void handleResult(AsyncResult responseResult, AmpEvent.AmpEventBuilder ampEventBuilder, RoutingContext routingContext, long startTime) { final boolean responseSucceeded = responseResult.succeeded(); + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final MetricName metricRequestStatus; final List errorMessages; @@ -280,16 +342,22 @@ private void handleResult(AsyncResult> respo ampEventBuilder.origin(origin); final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext, origin); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getLeft()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext, origin) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -315,11 +383,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); @@ -347,8 +416,7 @@ private void handleResult(AsyncResult> respo final int statusCode = status.code(); final AmpEvent ampEvent = ampEventBuilder.status(statusCode).errors(errorMessages).build(); - - final AuctionContext auctionContext = responseSucceeded ? responseResult.result().getRight() : null; + final AuctionContext auctionContext = ampEvent.getAuctionContext(); final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null; final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty(); @@ -361,7 +429,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 +438,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,12 +462,12 @@ 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); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext, String origin) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext, String origin) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -406,10 +479,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext, Stri // Add AMP headers responseHeaders.add("AMP-Access-Control-Allow-Source-Origin", origin) .add("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"); - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - final MultiMap headers = response.headers(); - headers.add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } 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..e0dbe2ea4e1 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,33 @@ 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.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; +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.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; 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 +40,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 +49,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,34 +57,48 @@ 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 HooksMetricsService hooksMetricsService; private final Clock clock; private final HttpInteractionLogger httpInteractionLogger; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public AuctionHandler(double logSamplingRate, AuctionRequestFactory auctionRequestFactory, ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, HttpInteractionLogger httpInteractionLogger, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, JacksonMapper mapper) { 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.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); 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,18 +110,34 @@ 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))) + .map(context -> addContextAndBidResponseToEvent(context, auctionEventBuilder, context)) + .map(context -> prepareSuccessfulResponse(context, routingContext)) + .compose(this::invokeExitpointHooks) + .map(context -> addContextAndBidResponseToEvent( + context.getAuctionContext(), auctionEventBuilder, context)) + .onComplete(result -> handleResult(result, auctionEventBuilder, routingContext, startTime)); + } - .map(this::updateAppAndNoCookieAndImpsMetrics) + private static R addContextAndBidResponseToEvent(AuctionContext context, + AuctionEvent.AuctionEventBuilder auctionEventBuilder, + R result) { - // In case of holdAuction Exception and auctionContext is not present below - .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) + auctionEventBuilder.auctionContext(context); + auctionEventBuilder.bidResponse(context.getBidResponse()); + return result; + } - .compose(exchangeService::holdAuction) - // populate event with updated context + 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 .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context)) - .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context)) - .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime)); + .compose(exchangeService::holdAuction); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -123,13 +162,54 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context return context; } - private void handleResult(AsyncResult responseResult, + private RawResponseContext prepareSuccessfulResponse(AuctionContext auctionContext, RoutingContext routingContext) { + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + + return RawResponseContext.builder() + .responseBody(mapper.encodeToString(auctionContext.getBidResponse())) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build(); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + + if (auctionContext.isAuctionSkipped()) { + return Future.succeededFuture(auctionContext) + .map(hooksMetricsService::updateHooksMetrics) + .map(rawResponseContext); + } + + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); + } + + 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 RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; + final AuctionContext auctionContext = rawResponseContext != null + ? rawResponseContext.getAuctionContext() + : null; + final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped(); final MetricName requestType = responseSucceeded ? auctionContext.getRequestTypeMetric() : MetricName.openrtb2web; @@ -140,16 +220,22 @@ private void handleResult(AsyncResult responseResult, final String body; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); - status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(responseResult.result().getBidResponse()); + + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); if (exception instanceof InvalidRequestException invalidRequestException) { metricRequestStatus = MetricName.badinput; @@ -172,11 +258,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,38 +292,44 @@ 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); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -244,10 +337,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } diff --git a/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java new file mode 100644 index 00000000000..d12d3fcd3b5 --- /dev/null +++ b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java @@ -0,0 +1,17 @@ +package org.prebid.server.handler.openrtb2; + +import io.vertx.core.MultiMap; +import lombok.Builder; +import lombok.Value; +import org.prebid.server.auction.model.AuctionContext; + +@Value +@Builder(toBuilder = true) +public class RawResponseContext { + + AuctionContext auctionContext; + + String responseBody; + + MultiMap responseHeaders; +} 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..0bb31bab72b 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java @@ -1,26 +1,33 @@ package org.prebid.server.handler.openrtb2; +import com.iab.openrtb.request.video.PodError; 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.VideoEvent; import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator; +import org.prebid.server.auction.AnalyticsTagsEnricher; import org.prebid.server.auction.ExchangeService; +import org.prebid.server.auction.HookDebugInfoEnricher; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.auction.VideoResponseFactory; 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.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.hooks.execution.HookStageExecutor; +import org.prebid.server.hooks.execution.model.HookStageExecutionResult; 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 +40,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 +51,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,32 +60,45 @@ 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 HooksMetricsService hooksMetricsService; private final Clock clock; private final PrebidVersionProvider prebidVersionProvider; + private final HookStageExecutor hookStageExecutor; private final JacksonMapper mapper; public VideoHandler(VideoRequestFactory videoRequestFactory, VideoResponseFactory videoResponseFactory, ExchangeService exchangeService, - CacheService cacheService, AnalyticsReporterDelegator analyticsDelegator, + CoreCacheService coreCacheService, + AnalyticsReporterDelegator analyticsDelegator, Metrics metrics, + HooksMetricsService hooksMetricsService, Clock clock, PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, 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.hooksMetricsService = Objects.requireNonNull(hooksMetricsService); this.clock = Objects.requireNonNull(clock); this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider); + this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); 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 @@ -98,13 +120,55 @@ public void handle(RoutingContext routingContext) { .map(contextToErrors -> addToEvent(contextToErrors.getData(), videoEventBuilder::auctionContext, contextToErrors)) - .map(result -> videoResponseFactory.toVideoResponse( - result.getData(), result.getData().getBidResponse(), - result.getPodErrors())) + .compose(contextToErrors -> + prepareSuccessfulResponse(contextToErrors, routingContext, videoEventBuilder) + .compose(this::invokeExitpointHooks) + .compose(context -> toVideoResponse(context.getAuctionContext(), contextToErrors.getPodErrors()) + .map(videoResponse -> + addToEvent(videoResponse, videoEventBuilder::bidResponse, context))) + .map(context -> + addToEvent(context.getAuctionContext(), videoEventBuilder::auctionContext, context))) + .onComplete(result -> handleResult(result, videoEventBuilder, routingContext, startTime)); + } + + private Future prepareSuccessfulResponse(WithPodErrors context, + RoutingContext routingContext, + VideoEvent.VideoEventBuilder videoEventBuilder) { + + final AuctionContext auctionContext = context.getData(); + final MultiMap responseHeaders = getCommonResponseHeaders(routingContext) + .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return toVideoResponse(auctionContext, context.getPodErrors()) .map(videoResponse -> addToEvent(videoResponse, videoEventBuilder::bidResponse, videoResponse)) - .onComplete(responseResult -> handleResult(responseResult, videoEventBuilder, routingContext, - startTime)); + .map(videoResponse -> RawResponseContext.builder() + .responseBody(mapper.encodeToString(videoResponse)) + .responseHeaders(responseHeaders) + .auctionContext(auctionContext) + .build()); + } + + private Future toVideoResponse(AuctionContext auctionContext, List podErrors) { + return Future.succeededFuture( + videoResponseFactory.toVideoResponse(auctionContext, auctionContext.getBidResponse(), podErrors)); + } + + private Future invokeExitpointHooks(RawResponseContext rawResponseContext) { + final AuctionContext auctionContext = rawResponseContext.getAuctionContext(); + return hookStageExecutor.executeExitpointStage( + rawResponseContext.getResponseHeaders(), + rawResponseContext.getResponseBody(), + auctionContext) + .map(HookStageExecutionResult::getPayload) + .compose(payload -> Future.succeededFuture(auctionContext) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) + .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) + .map(hooksMetricsService::updateHooksMetrics) + .map(context -> RawResponseContext.builder() + .auctionContext(context) + .responseHeaders(payload.responseHeaders()) + .responseBody(payload.responseBody()) + .build())); } private static R addToEvent(T field, Consumer consumer, R result) { @@ -112,7 +176,7 @@ private static R addToEvent(T field, Consumer consumer, R result) { return result; } - private void handleResult(AsyncResult responseResult, + private void handleResult(AsyncResult responseResult, VideoEvent.VideoEventBuilder videoEventBuilder, RoutingContext routingContext, long startTime) { @@ -122,24 +186,30 @@ private void handleResult(AsyncResult responseResult, final List errorMessages; final HttpResponseStatus status; final String body; - final VideoResponse videoResponse = responseSucceeded ? responseResult.result() : null; + final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null; final HttpServerResponse response = routingContext.response(); - enrichResponseWithCommonHeaders(routingContext); + final MultiMap responseHeaders = response.headers(); if (responseSucceeded) { metricRequestStatus = MetricName.ok; errorMessages = Collections.emptyList(); status = HttpResponseStatus.OK; - enrichWithSuccessfulHeaders(response); - body = mapper.encodeToString(videoResponse); + rawResponseContext.getResponseHeaders() + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + body = rawResponseContext.getResponseBody(); } else { + getCommonResponseHeaders(routingContext) + .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty( + responseHeaders, header.getKey(), header.getValue())); + final Throwable exception = responseResult.cause(); 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 +218,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 +264,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,12 +298,12 @@ 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); } - private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { - final MultiMap responseHeaders = routingContext.response().headers(); + private MultiMap getCommonResponseHeaders(RoutingContext routingContext) { + final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap(); HttpUtil.addHeaderIfValueIsNotEmpty( responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord()); @@ -241,10 +311,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) { if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) { responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1"); } - } - private void enrichWithSuccessfulHeaders(HttpServerResponse response) { - response.headers() - .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON); + return responseHeaders; } } 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/GeoLocationHealthChecker.java b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java index 6243f9ed8c7..97bd43c4abf 100644 --- a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java +++ b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java @@ -1,7 +1,7 @@ package org.prebid.server.health; import io.vertx.core.Vertx; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.model.Status; import org.prebid.server.health.model.StatusResponse; 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/health/model/StatusResponse.java b/src/main/java/org/prebid/server/health/model/StatusResponse.java index 7daca0ea241..e7a6b5d6f08 100644 --- a/src/main/java/org/prebid/server/health/model/StatusResponse.java +++ b/src/main/java/org/prebid/server/health/model/StatusResponse.java @@ -1,12 +1,10 @@ package org.prebid.server.health.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.time.ZonedDateTime; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class StatusResponse { String status; 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..18d52b64c99 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -4,45 +4,44 @@ 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; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.log.ConditionalLogger; import java.time.Clock; +import java.util.Map; import java.util.concurrent.TimeoutException; -import java.util.function.Function; import java.util.function.Supplier; class GroupExecutor { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class)); - private final Vertx vertx; private final Clock clock; + private final Map modulesExecution; private ExecutionGroup group; private PAYLOAD initialPayload; - private Function> hookProvider; + private HookProvider hookProvider; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; - private GroupExecutor(Vertx vertx, Clock clock) { + private GroupExecutor(Vertx vertx, Clock clock, Map modulesExecution) { this.vertx = vertx; this.clock = clock; + this.modulesExecution = modulesExecution; } public static GroupExecutor create( Vertx vertx, - Clock clock) { + Clock clock, + Map modulesExecution) { - return new GroupExecutor<>(vertx, clock); + return new GroupExecutor<>(vertx, clock, modulesExecution); } public GroupExecutor withGroup(ExecutionGroup group) { @@ -55,7 +54,7 @@ public GroupExecutor withInitialPayload(PAYLOAD initialPayload return this; } - public GroupExecutor withHookProvider(Function> hookProvider) { + public GroupExecutor withHookProvider(HookProvider hookProvider) { this.hookProvider = hookProvider; return this; } @@ -82,11 +81,15 @@ public Future> execute() { Future> groupFuture = Future.succeededFuture(initialGroupResult); for (final HookId hookId : group.getHookSequence()) { - final Hook hook = hookProvider.apply(hookId); + if (!modulesExecution.get(hookId.getModuleCode())) { + continue; + } + + final Future> hookFuture = hook(hookId); final long startTime = clock.millis(); - final Future> invocationResult = - executeHook(hook, group.getTimeout(), initialGroupResult, hookId); + final Future> invocationResult = hookFuture + .compose(hook -> executeHook(hook, group.getTimeout(), initialGroupResult, hookId)); groupFuture = groupFuture.compose(groupResult -> applyInvocationResult(invocationResult, hookId, startTime, groupResult)); @@ -95,23 +98,21 @@ public Future> execute() { return groupFuture.recover(GroupExecutor::restoreResultFromRejection); } - private Future> executeHook( - Hook hook, - Long timeout, - GroupResult groupResult, - HookId hookId) { - - if (hook == null) { - conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); - - return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled")); + private Future> hook(HookId hookId) { + try { + return Future.succeededFuture(hookProvider.apply(hookId)); + } catch (Exception e) { + return Future.failedFuture(new FailedException(e.getMessage())); } + } + + private Future> executeHook(Hook hook, + Long timeout, + GroupResult groupResult, + HookId hookId) { - return executeWithTimeout( - () -> hook.call( - groupResult.payload(), - invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId))), - timeout); + final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId)); + return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout); } private Future executeWithTimeout(Supplier> action, Long timeout) { 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..718548ae15b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java @@ -1,8 +1,9 @@ package org.prebid.server.hooks.execution; -import io.vertx.core.logging.LoggerFactory; import lombok.Getter; import lombok.experimental.Accessors; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionStatus; import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; @@ -13,9 +14,12 @@ 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.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeoutException; @Accessors(fluent = true) @@ -33,6 +37,8 @@ class GroupResult { private final boolean rejectAllowed; + private final Map> rejections = new HashMap<>(); + private final List hookExecutionOutcomes = new ArrayList<>(); private GroupResult(T payload, boolean rejectAllowed) { @@ -52,6 +58,8 @@ public GroupResult applyInvocationResult(InvocationResult invocationResult if (invocationResult.status() == InvocationStatus.success && invocationResult.action() != null) { try { applyAction(hookId, invocationResult.action(), invocationResult.payloadUpdate()); + MapUtils.emptyIfNull(invocationResult.rejections()).forEach((bidder, rejectionList) -> + rejections.computeIfAbsent(bidder, key -> new ArrayList<>()).addAll(rejectionList)); } catch (Exception e) { hookExecutionOutcomes.add(toExecutionOutcome(e, hookId, executionTime)); @@ -173,6 +181,7 @@ private static ExecutionAction toExecutionAction(InvocationAction action) { case reject -> ExecutionAction.reject; case update -> ExecutionAction.update; case no_action -> ExecutionAction.no_action; + case no_invocation -> ExecutionAction.no_invocation; }; } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java index 754e3925b11..f58b7f136c7 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java @@ -1,38 +1,46 @@ package org.prebid.server.hooks.execution; +import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.StageWithHookType; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.Collection; import java.util.Objects; -/** - * Provides simple access to all {@link Hook}s registered in application. - */ public class HookCatalog { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(HookCatalog.class)); + private final Collection modules; public HookCatalog(Collection modules) { this.modules = Objects.requireNonNull(modules); } - public > HOOK hookById( - String moduleCode, - String hookImplCode, - StageWithHookType stage) { + public > HOOK hookById(HookId hookId, + StageWithHookType stage) { final Class clazz = stage.hookType(); return modules.stream() - .filter(module -> Objects.equals(module.code(), moduleCode)) + .filter(module -> Objects.equals(module.code(), hookId.getModuleCode())) .map(Module::hooks) .flatMap(Collection::stream) - .filter(hook -> Objects.equals(hook.code(), hookImplCode)) + .filter(hook -> Objects.equals(hook.code(), hookId.getHookImplCode())) .filter(clazz::isInstance) .map(clazz::cast) .findFirst() - .orElse(null); + .orElseThrow(() -> { + logAbsentHook(hookId); + return new IllegalArgumentException("Hook implementation does not exist or disabled"); + }); + } + + private static void logAbsentHook(HookId hookId) { + conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); } } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index 81f44e3a528..4f8a1f3f209 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -1,18 +1,27 @@ package org.prebid.server.hooks.execution; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; +import io.vertx.core.MultiMap; import io.vertx.core.Vertx; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.collections4.map.DefaultedMap; 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.BidRejectionTracker; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.execution.Timeout; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ABTest; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; @@ -22,6 +31,8 @@ import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.execution.provider.abtest.ABTestHookProvider; import org.prebid.server.hooks.execution.v1.InvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -31,6 +42,7 @@ import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl; +import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; @@ -41,24 +53,31 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountHooksConfiguration; +import org.prebid.server.settings.model.HooksAdminConfig; import java.time.Clock; 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.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; public class HookStageExecutor { private static final String ENTITY_HTTP_REQUEST = "http-request"; + private static final String ENTITY_HTTP_RESPONSE = "http-response"; private static final String ENTITY_AUCTION_REQUEST = "auction-request"; private static final String ENTITY_AUCTION_RESPONSE = "auction-response"; private static final String ENTITY_ALL_PROCESSED_BID_RESPONSES = "all-processed-bid-responses"; @@ -66,17 +85,25 @@ public class HookStageExecutor { private final ExecutionPlan hostExecutionPlan; private final ExecutionPlan defaultAccountExecutionPlan; + private final Map hostModuleExecution; private final HookCatalog hookCatalog; private final TimeoutFactory timeoutFactory; private final Vertx vertx; private final Clock clock; + private final ObjectMapper mapper; + private final boolean isConfigToInvokeRequired; + private final double logSamplingRate; private HookStageExecutor(ExecutionPlan hostExecutionPlan, ExecutionPlan defaultAccountExecutionPlan, + Map hostModuleExecution, HookCatalog hookCatalog, TimeoutFactory timeoutFactory, Vertx vertx, - Clock clock) { + Clock clock, + ObjectMapper mapper, + boolean isConfigToInvokeRequired, + double logSamplingRate) { this.hostExecutionPlan = hostExecutionPlan; this.defaultAccountExecutionPlan = defaultAccountExecutionPlan; @@ -84,42 +111,99 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan, this.timeoutFactory = timeoutFactory; this.vertx = vertx; this.clock = clock; + this.mapper = mapper; + this.isConfigToInvokeRequired = isConfigToInvokeRequired; + this.hostModuleExecution = hostModuleExecution; + this.logSamplingRate = logSamplingRate; } public static HookStageExecutor create(String hostExecutionPlan, String defaultAccountExecutionPlan, + Map hostModuleExecution, HookCatalog hookCatalog, TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + boolean isConfigToInvokeRequired, + double logSamplingRate) { + + Objects.requireNonNull(hookCatalog); + Objects.requireNonNull(mapper); return new HookStageExecutor( - parseAndValidateExecutionPlan( - hostExecutionPlan, - Objects.requireNonNull(mapper), - Objects.requireNonNull(hookCatalog)), + parseAndValidateExecutionPlan(hostExecutionPlan, mapper, hookCatalog), parseAndValidateExecutionPlan(defaultAccountExecutionPlan, mapper, hookCatalog), + hostModuleExecution, hookCatalog, Objects.requireNonNull(timeoutFactory), Objects.requireNonNull(vertx), - Objects.requireNonNull(clock)); + Objects.requireNonNull(clock), + mapper.mapper(), + isConfigToInvokeRequired, + logSamplingRate); + } + + private static ExecutionPlan parseAndValidateExecutionPlan(String executionPlan, + JacksonMapper mapper, + HookCatalog hookCatalog) { + + return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog); + } + + private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { + if (StringUtils.isBlank(executionPlan)) { + return ExecutionPlan.empty(); + } + + try { + return mapper.decodeValue(executionPlan, ExecutionPlan.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); + } + } + + private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) { + MapUtils.emptyIfNull(plan.getEndpoints()).values().stream() + .map(EndpointExecutionPlan::getStages) + .map(Map::entrySet) + .flatMap(Collection::stream) + .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream() + .map(ExecutionGroup::getHookSequence) + .flatMap(Collection::stream) + .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); + + return plan; + } + + private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { + try { + hookCatalog.hookById(hookId, StageWithHookType.forStage(stage)); + } catch (Throwable e) { + throw new IllegalArgumentException( + "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" + .formatted(stage, hookId)); + } } public Future> executeEntrypointStage( CaseInsensitiveMultiMap queryParams, CaseInsensitiveMultiMap headers, String body, - HookExecutionContext context) { + AuctionContext auctionContext) { + final HookExecutionContext context = auctionContext.getHookExecutionContext(); final Endpoint endpoint = context.getEndpoint(); return stageExecutor(StageWithHookType.ENTRYPOINT, ENTITY_HTTP_REQUEST, context) .withExecutionPlan(planForEntrypointStage(endpoint)) + .withHookProvider(hookProviderForEntrypointStage(context)) .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body)) .withInvocationContextProvider(invocationContextProvider(endpoint)) + .withModulesExecution(DefaultedMap.defaultedMap(hostModuleExecution, true)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAll(auctionContext, result)); } public Future> executeRawAuctionRequestStage( @@ -137,7 +221,8 @@ public Future> executeRawAuction .withInitialPayload(AuctionRequestPayloadImpl.of(bidRequest)) .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAll(auctionContext, result)); } public Future> executeProcessedAuctionRequestStage( @@ -155,7 +240,8 @@ public Future> executeProcessedA .withInitialPayload(AuctionRequestPayloadImpl.of(bidRequest)) .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAll(auctionContext, result)); } public Future> executeBidderRequestStage( @@ -173,7 +259,8 @@ public Future> executeBidderReque .withInitialPayload(BidderRequestPayloadImpl.of(bidderRequest.getBidRequest())) .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAllIgnoringUnknowns(auctionContext, result)); } public Future> executeRawBidderResponseStage( @@ -193,7 +280,8 @@ public Future> executeRawBidderR .withInitialPayload(BidderResponsePayloadImpl.of(bids)) .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAllIgnoringUnknowns(auctionContext, result)); } public Future> executeProcessedBidderResponseStage( @@ -212,7 +300,8 @@ public Future> executeProcessedB .withInitialPayload(BidderResponsePayloadImpl.of(bids)) .withInvocationContextProvider(bidderInvocationContextProvider(endpoint, auctionContext, bidder)) .withRejectAllowed(true) - .execute(); + .execute() + .map(result -> rejectAllIgnoringUnknowns(auctionContext, result)); } public Future> executeAllProcessedBidResponsesStage( @@ -230,7 +319,8 @@ public Future> execute .withInitialPayload(AllProcessedBidResponsesPayloadImpl.of(bidderResponses)) .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) .withRejectAllowed(false) - .execute(); + .execute() + .map(result -> rejectAllIgnoringUnknowns(auctionContext, result)); } public Future> executeAuctionResponseStage( @@ -249,12 +339,28 @@ public Future> executeAuctionRe .execute(); } + public Future> executeExitpointStage(MultiMap responseHeaders, + String responseBody, + AuctionContext auctionContext) { + + final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), EMPTY_ACCOUNT); + final HookExecutionContext context = auctionContext.getHookExecutionContext(); + + final Endpoint endpoint = context.getEndpoint(); + + return stageExecutor(StageWithHookType.EXITPOINT, ENTITY_HTTP_RESPONSE, context, account, endpoint) + .withInitialPayload(ExitpointPayloadImpl.of(responseHeaders, responseBody)) + .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext)) + .withRejectAllowed(false) + .execute(); + } + private StageExecutor stageExecutor( StageWithHookType> stage, String entity, HookExecutionContext context) { - return StageExecutor.create(hookCatalog, vertx, clock) + return StageExecutor.create(vertx, clock) .withStage(stage) .withEntity(entity) .withHookExecutionContext(context); @@ -268,53 +374,30 @@ private StageExecutor stageToPlan.getValue().getGroups().stream() - .map(ExecutionGroup::getHookSequence) - .flatMap(Collection::stream) - .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); - - return plan; - } - - private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { - final Hook hook = hookCatalog.hookById( - hookId.getModuleCode(), - hookId.getHookImplCode(), - StageWithHookType.forStage(stage)); - - if (hook == null) { - throw new IllegalArgumentException( - "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" - .formatted(stage, hookId)); - } - } - - private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { - if (StringUtils.isBlank(executionPlan)) { - return ExecutionPlan.empty(); + private Map modulesExecutionForAccount(Account account) { + final Map accountModulesExecution = Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getAdmin) + .map(HooksAdminConfig::getModuleExecution) + .orElse(Collections.emptyMap()); + + final Map resultModulesExecution = new HashMap<>(accountModulesExecution); + + if (isConfigToInvokeRequired) { + Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getModules) + .map(Map::keySet) + .stream() + .flatMap(Collection::stream) + .forEach(module -> resultModulesExecution.computeIfAbsent(module, key -> true)); } - try { - return mapper.decodeValue(executionPlan, ExecutionPlan.class); - } catch (DecodeException e) { - throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); - } + resultModulesExecution.putAll(hostModuleExecution); + return DefaultedMap.defaultedMap(resultModulesExecution, !isConfigToInvokeRequired); } private StageExecutionPlan planForEntrypointStage(Endpoint endpoint) { @@ -346,8 +429,7 @@ private StageExecutionPlan effectiveStagePlanFrom( } private static StageExecutionPlan stagePlanFrom(ExecutionPlan executionPlan, Endpoint endpoint, Stage stage) { - return executionPlan - .getEndpoints() + return MapUtils.emptyIfNull(executionPlan.getEndpoints()) .getOrDefault(endpoint, EndpointExecutionPlan.empty()) .getStages() .getOrDefault(stage, StageExecutionPlan.empty()); @@ -361,6 +443,34 @@ private ExecutionPlan effectiveExecutionPlanFor(Account account) { return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan; } + private HookProvider hookProviderForEntrypointStage( + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(StageWithHookType.ENTRYPOINT), + abTestsForEntrypointStage(), + context, + mapper); + } + + private HookProvider hookProvider( + StageWithHookType> stage, + Account account, + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(stage), + abTests(account), + context, + mapper); + } + + private HookProvider defaultHookProvider( + StageWithHookType> stage) { + + return hookId -> hookCatalog.hookById(hookId, stage); + } + private InvocationContextProvider invocationContextProvider(Endpoint endpoint) { return (timeout, hookId, moduleContext) -> invocationContext(endpoint, timeout); } @@ -406,10 +516,78 @@ private Timeout createTimeout(Long timeout) { } private static ObjectNode accountConfigFor(Account account, HookId hookId) { - final AccountHooksConfiguration accountHooksConfiguration = account.getHooks(); + final AccountHooksConfiguration accountHooksConfiguration = account != null ? account.getHooks() : null; final Map modulesConfiguration = accountHooksConfiguration != null ? accountHooksConfiguration.getModules() : Collections.emptyMap(); return modulesConfiguration != null ? modulesConfiguration.get(hookId.getModuleCode()) : null; } + + protected List abTestsForEntrypointStage() { + return ListUtils.emptyIfNull(hostExecutionPlan.getAbTests()).stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList(); + } + + private static boolean isABTestEnabled(ABTest abTest) { + return abTest != null && abTest.isEnabled(); + } + + protected List abTests(Account account) { + return abTestsFromAccount(account) + .or(() -> abTestsFromHostConfig(account.getId())) + .orElse(Collections.emptyList()); + } + + private Optional> abTestsFromAccount(Account account) { + return Optional.of(effectiveExecutionPlanFor(account)) + .map(ExecutionPlan::getAbTests) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList()); + } + + private Optional> abTestsFromHostConfig(String accountId) { + return Optional.ofNullable(hostExecutionPlan.getAbTests()) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .filter(abTest -> isABTestApplicable(abTest, accountId)) + .toList()); + } + + private static boolean isABTestApplicable(ABTest abTest, String account) { + final Set accounts = abTest.getAccounts(); + return CollectionUtils.isEmpty(accounts) || accounts.contains(account); + } + + //todo: should it be more strict? e.g. allowing rejecting only imps/bids on the particular stages + + private HookStageExecutionResult rejectAll(AuctionContext auctionContext, + HookStageExecutionResult result) { + + result.getRejections() + .forEach((bidder, rejectedList) -> auctionContext.getBidRejectionTrackers().computeIfAbsent( + bidder, + key -> new BidRejectionTracker( + key, + rejectedList.stream().map(Rejection::impId).collect(Collectors.toSet()), + logSamplingRate)) + .reject(rejectedList)); + + return result; + } + + private HookStageExecutionResult rejectAllIgnoringUnknowns(AuctionContext auctionContext, + HookStageExecutionResult result) { + + result.getRejections() + .forEach((bidder, rejectedList) -> auctionContext.getBidRejectionTrackers().computeIfPresent( + bidder, + (key, value) -> { + value.reject(rejectedList); + return value; + })); + + return result; + } } diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java index f4f4a8176de..2db76e7c657 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -7,38 +7,39 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import java.time.Clock; import java.util.ArrayList; +import java.util.Map; class StageExecutor { - private final HookCatalog hookCatalog; private final Vertx vertx; private final Clock clock; private StageWithHookType> stage; private String entity; private StageExecutionPlan executionPlan; + private HookProvider hookProvider; private PAYLOAD initialPayload; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; + private Map modulesExecution; - private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { - this.hookCatalog = hookCatalog; + private StageExecutor(Vertx vertx, Clock clock) { this.vertx = vertx; this.clock = clock; } public static StageExecutor create( - HookCatalog hookCatalog, Vertx vertx, Clock clock) { - return new StageExecutor<>(hookCatalog, vertx, clock); + return new StageExecutor<>(vertx, clock); } public StageExecutor withStage(StageWithHookType> stage) { @@ -56,6 +57,11 @@ public StageExecutor withExecutionPlan(StageExecutionPlan exec return this; } + public StageExecutor withHookProvider(HookProvider hookProvider) { + this.hookProvider = hookProvider; + return this; + } + public StageExecutor withInitialPayload(PAYLOAD initialPayload) { this.initialPayload = initialPayload; return this; @@ -78,6 +84,11 @@ public StageExecutor withRejectAllowed(boolean rejectAllowed) return this; } + public StageExecutor withModulesExecution(Map modulesExecution) { + this.modulesExecution = modulesExecution; + return this; + } + public Future> execute() { Future> stageFuture = Future.succeededFuture(StageResult.of(initialPayload, entity)); @@ -94,11 +105,10 @@ public Future> execute() { } private Future> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) { - return GroupExecutor.create(vertx, clock) + return GroupExecutor.create(vertx, clock, modulesExecution) .withGroup(group) .withInitialPayload(initialPayload) - .withHookProvider( - hookId -> hookCatalog.hookById(hookId.getModuleCode(), hookId.getHookImplCode(), stage)) + .withHookProvider(hookProvider) .withInvocationContextProvider(invocationContextProvider) .withHookExecutionContext(hookExecutionContext) .withRejectAllowed(rejectAllowed) @@ -126,6 +136,6 @@ private HookStageExecutionResult toHookStageExecutionResult(StageResult return stageResult.shouldReject() ? HookStageExecutionResult.reject() - : HookStageExecutionResult.success(stageResult.payload()); + : HookStageExecutionResult.success(stageResult.payload(), stageResult.rejections()); } } diff --git a/src/main/java/org/prebid/server/hooks/execution/StageResult.java b/src/main/java/org/prebid/server/hooks/execution/StageResult.java index 695747e73ec..5183a1571ad 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageResult.java @@ -2,11 +2,15 @@ import lombok.Getter; import lombok.experimental.Accessors; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Accessors(fluent = true) @Getter @@ -47,4 +51,20 @@ private List groupExecutionOutcomes() { .map(GroupResult::toGroupExecutionOutcome) .toList(); } + + public Map> rejections() { + return groupResults.stream() + .map(GroupResult::rejections) + .reduce(StageResult::collectionMerge) + .orElse(Collections.emptyMap()); + } + + private static Map> collectionMerge(Map> left, + Map> right) { + + final Map> merged = new HashMap<>(); + left.forEach((key, value) -> merged.put(key, new ArrayList<>(value))); + right.forEach((key, value) -> merged.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value)); + return Collections.unmodifiableMap(merged); + } } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java new file mode 100644 index 00000000000..67d64190101 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder +@Value +public class ABTest { + + boolean enabled; + + @JsonProperty("module-code") + @JsonAlias("module_code") + String moduleCode; + + Set accounts; + + @JsonProperty("percent-active") + @JsonAlias("percent_active") + Integer percentActive; + + @JsonProperty("log-analytics-tag") + @JsonAlias("log_analytics_tag") + Boolean logAnalyticsTag; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java index 5e13aa3f14c..886cec114e8 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java @@ -2,5 +2,5 @@ public enum ExecutionAction { - no_action, update, reject + no_action, update, reject, no_invocation } 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/ExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java index 5d0af8b8e23..7137cef162d 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java @@ -1,15 +1,19 @@ package org.prebid.server.hooks.execution.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import org.prebid.server.model.Endpoint; -import java.util.Collections; +import java.util.List; import java.util.Map; @Value(staticConstructor = "of") public class ExecutionPlan { - private static final ExecutionPlan EMPTY = of(Collections.emptyMap()); + private static final ExecutionPlan EMPTY = of(null, null); + + @JsonProperty("abtests") + List abTests; Map endpoints; 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/HookStageExecutionResult.java b/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java index 5e867add4ef..d88d2599bf8 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/HookStageExecutionResult.java @@ -1,6 +1,11 @@ package org.prebid.server.hooks.execution.model; import lombok.Value; +import org.prebid.server.auction.model.Rejection; + +import java.util.Collections; +import java.util.List; +import java.util.Map; @Value(staticConstructor = "of") public class HookStageExecutionResult { @@ -9,11 +14,18 @@ public class HookStageExecutionResult { PAYLOAD payload; + Map> rejections; + + public static HookStageExecutionResult success(PAYLOAD payload, + Map> rejections) { + return of(false, payload, rejections); + } + public static HookStageExecutionResult success(PAYLOAD payload) { - return of(false, payload); + return of(false, payload, Collections.emptyMap()); } public static HookStageExecutionResult reject() { - return of(true, null); + return of(true, null, Collections.emptyMap()); } } 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..bb7c151ed6f 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,39 @@ 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, + + exitpoint } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java index f8738d2c2db..961450a3c3f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java @@ -10,6 +10,7 @@ import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook; import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.exitpoint.ExitpointHook; public interface StageWithHookType> { @@ -29,6 +30,8 @@ public interface StageWithHookType(Stage.all_processed_bid_responses, AllProcessedBidResponsesHook.class); StageWithHookType AUCTION_RESPONSE = new StageWithHookTypeImpl<>(Stage.auction_response, AuctionResponseHook.class); + StageWithHookType EXITPOINT = + new StageWithHookTypeImpl<>(Stage.exitpoint, ExitpointHook.class); Stage stage(); @@ -44,6 +47,7 @@ public interface StageWithHookType ALL_PROCESSED_BID_RESPONSES; case processed_bidder_response -> PROCESSED_BIDDER_RESPONSE; case auction_response -> AUCTION_RESPONSE; + case exitpoint -> EXITPOINT; }; } } diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java index 1f4f7a7f521..2765a950c53 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookTypeImpl.java @@ -1,17 +1,15 @@ package org.prebid.server.hooks.execution.model; -import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Value; import lombok.experimental.Accessors; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; -@AllArgsConstructor -@Getter @Accessors(fluent = true) +@Value class StageWithHookTypeImpl> implements StageWithHookType { - private final Stage stage; + Stage stage; - private final Class hookType; + Class hookType; } diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java new file mode 100644 index 00000000000..83297c26396 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.execution.provider; + +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.function.Function; + +public interface HookProvider + extends Function> { +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java new file mode 100644 index 00000000000..fc7865d1211 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java @@ -0,0 +1,155 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.vertx.core.Future; +import org.prebid.server.auction.model.Rejection; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.v1.Hook; +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.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class ABTestHook implements Hook { + + private static final String ANALYTICS_ACTIVITY_NAME = "core-module-abtests"; + + private final String moduleName; + private final Hook hook; + private final boolean shouldInvokeHook; + private final boolean logABTestAnalyticsTag; + private final ObjectMapper mapper; + + public ABTestHook(String moduleName, + Hook hook, + boolean shouldInvokeHook, + boolean logABTestAnalyticsTag, + ObjectMapper mapper) { + + this.moduleName = Objects.requireNonNull(moduleName); + this.hook = Objects.requireNonNull(hook); + this.shouldInvokeHook = shouldInvokeHook; + this.logABTestAnalyticsTag = logABTestAnalyticsTag; + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public String code() { + return hook.code(); + } + + @Override + public Future> call(PAYLOAD payload, CONTEXT invocationContext) { + if (!shouldInvokeHook) { + return skippedResult(); + } + + final Future> invocationResultFuture = hook.call(payload, invocationContext); + return logABTestAnalyticsTag + ? invocationResultFuture.map(this::enrichWithABTestAnalyticsTag) + : invocationResultFuture; + } + + private Future> skippedResult() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_invocation) + .analyticsTags(logABTestAnalyticsTag ? tags("skipped") : null) + .build()); + } + + private Tags tags(String status) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ANALYTICS_ACTIVITY_NAME, + "success", + Collections.singletonList(ResultImpl.of(status, analyticsValues(), null))))); + } + + private ObjectNode analyticsValues() { + final ObjectNode values = mapper.createObjectNode(); + values.put("module", moduleName); + return values; + } + + private InvocationResult enrichWithABTestAnalyticsTag(InvocationResult invocationResult) { + return new InvocationResultWithAdditionalTags<>(invocationResult, tags("run")); + } + + private record InvocationResultWithAdditionalTags(InvocationResult invocationResult, + Tags additionalTags) + implements InvocationResult { + + @Override + public InvocationStatus status() { + return invocationResult.status(); + } + + @Override + public String message() { + return invocationResult.message(); + } + + @Override + public InvocationAction action() { + return invocationResult.action(); + } + + @Override + public PayloadUpdate payloadUpdate() { + return invocationResult.payloadUpdate(); + } + + @Override + public List errors() { + return invocationResult.errors(); + } + + @Override + public List warnings() { + return invocationResult.warnings(); + } + + @Override + public Map> rejections() { + return invocationResult.rejections(); + } + + @Override + public List debugMessages() { + return invocationResult.debugMessages(); + } + + @Override + public Object moduleContext() { + return invocationResult.moduleContext(); + } + + @Override + public Tags analyticsTags() { + return new TagsUnion(invocationResult.analyticsTags(), additionalTags); + } + } + + private record TagsUnion(Tags left, Tags right) implements Tags { + + @Override + public List activities() { + return left != null + ? ListUtil.union(left.activities(), right.activities()) + : right.activities(); + } + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java new file mode 100644 index 00000000000..6a833ab2833 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java @@ -0,0 +1,87 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.hooks.execution.model.ABTest; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +public class ABTestHookProvider implements HookProvider { + + private final HookProvider innerHookProvider; + private final List abTests; + private final HookExecutionContext context; + private final ObjectMapper mapper; + + public ABTestHookProvider(HookProvider innerHookProvider, + List abTests, + HookExecutionContext context, + ObjectMapper mapper) { + + this.innerHookProvider = Objects.requireNonNull(innerHookProvider); + this.abTests = Objects.requireNonNull(abTests); + this.context = Objects.requireNonNull(context); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Hook apply(HookId hookId) { + final Hook hook = innerHookProvider.apply(hookId); + + final String moduleCode = hookId.getModuleCode(); + final ABTest abTest = searchForABTest(moduleCode); + if (abTest == null) { + return hook; + } + + return new ABTestHook<>( + moduleCode, + hook, + shouldInvokeHook(moduleCode, abTest), + BooleanUtils.isNotFalse(abTest.getLogAnalyticsTag()), + mapper); + } + + private ABTest searchForABTest(String moduleCode) { + return abTests.stream() + .filter(abTest -> moduleCode.equals(abTest.getModuleCode())) + .findFirst() + .orElse(null); + } + + protected boolean shouldInvokeHook(String moduleCode, ABTest abTest) { + final HookExecutionOutcome hookExecutionOutcome = searchForPreviousExecution(moduleCode); + if (hookExecutionOutcome != null) { + return hookExecutionOutcome.getAction() != ExecutionAction.no_invocation; + } + + final int percent = ObjectUtils.defaultIfNull(abTest.getPercentActive(), 100); + return ThreadLocalRandom.current().nextInt(100) < percent; + } + + private HookExecutionOutcome searchForPreviousExecution(String moduleCode) { + return context.getStageOutcomes().values().stream() + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getHookId().getModuleCode().equals(moduleCode)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java index 6ed23ef8980..99399d5ba6b 100644 --- a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java @@ -2,7 +2,7 @@ import lombok.Value; import lombok.experimental.Accessors; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.model.Endpoint; diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java new file mode 100644 index 00000000000..694c50e8104 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.execution.v1; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.auction.model.Rejection; +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; +import java.util.Map; + +@Accessors(fluent = true) +@Builder +@Value +public class InvocationResultImpl implements InvocationResult { + + InvocationStatus status; + + String message; + + InvocationAction action; + + PayloadUpdate payloadUpdate; + + List errors; + + List warnings; + + List debugMessages; + + Map> rejections; + + Object moduleContext; + + Tags analyticsTags; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java new file mode 100644 index 00000000000..4c9747e16bc --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.execution.v1.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ActivityImpl implements Activity { + + String name; + + String status; + + List results; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java new file mode 100644 index 00000000000..884603b4717 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.execution.v1.analytics; + +import lombok.Builder; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; + +import java.util.List; + +@Accessors(fluent = true) +@Builder +@Value +public class AppliedToImpl implements AppliedTo { + + List impIds; + + List bidders; + + boolean request; + + boolean response; + + List bidIds; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java new file mode 100644 index 00000000000..c16397e894c --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.execution.v1.analytics; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.AppliedTo; +import org.prebid.server.hooks.v1.analytics.Result; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ResultImpl implements Result { + + String status; + + ObjectNode values; + + AppliedTo appliedTo; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java new file mode 100644 index 00000000000..f068f28dcef --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.v1.analytics; + +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.List; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class TagsImpl implements Tags { + + List activities; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java new file mode 100644 index 00000000000..d57080f6b90 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.execution.v1.exitpoint; + +import io.vertx.core.MultiMap; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class ExitpointPayloadImpl implements ExitpointPayload { + + MultiMap responseHeaders; + + String responseBody; +} diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java index 29b22bf1b3d..821b21a730b 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java @@ -2,5 +2,5 @@ public enum InvocationAction { - no_action, update, reject + no_action, update, reject, no_invocation } diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java index 7c3b6c922d3..22493ea8a07 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java @@ -1,6 +1,6 @@ package org.prebid.server.hooks.v1; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.model.Endpoint; public interface InvocationContext { diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java b/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java index 979ff697316..efdb308d34c 100644 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java +++ b/src/main/java/org/prebid/server/hooks/v1/InvocationResult.java @@ -1,8 +1,10 @@ package org.prebid.server.hooks.v1; +import org.prebid.server.auction.model.Rejection; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.List; +import java.util.Map; public interface InvocationResult { @@ -18,6 +20,8 @@ public interface InvocationResult { List warnings(); + Map> rejections(); + List debugMessages(); Object moduleContext(); diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java new file mode 100644 index 00000000000..02e36af17a5 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +public interface ExitpointHook extends Hook { +} diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java new file mode 100644 index 00000000000..ae596949fa0 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java @@ -0,0 +1,10 @@ +package org.prebid.server.hooks.v1.exitpoint; + +import io.vertx.core.MultiMap; + +public interface ExitpointPayload { + + MultiMap responseHeaders(); + + String responseBody(); +} diff --git a/src/main/java/org/prebid/server/json/JsonMerger.java b/src/main/java/org/prebid/server/json/JsonMerger.java index 3943b703acf..999867b75b1 100644 --- a/src/main/java/org/prebid/server/json/JsonMerger.java +++ b/src/main/java/org/prebid/server/json/JsonMerger.java @@ -3,9 +3,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.github.fge.jsonpatch.JsonPatchException; -import com.github.fge.jsonpatch.mergepatch.JsonMergePatch; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.exception.InvalidRequestException; +import org.prebid.server.json.merge.JsonMergePatch; import java.io.IOException; import java.util.Objects; diff --git a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java index 96189d6706f..e34ff41c0c0 100644 --- a/src/main/java/org/prebid/server/json/ObjectMapperProvider.java +++ b/src/main/java/org/prebid/server/json/ObjectMapperProvider.java @@ -2,11 +2,13 @@ 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; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.blackbird.BlackbirdModule; public final class ObjectMapperProvider { @@ -17,9 +19,12 @@ 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 JavaTimeModule()) .registerModule(new BlackbirdModule()) .registerModule(new ZonedDateTimeModule()) .registerModule(new MissingJsonNodeModule()) diff --git a/src/main/java/org/prebid/server/json/merge/JsonMergePatch.java b/src/main/java/org/prebid/server/json/merge/JsonMergePatch.java new file mode 100644 index 00000000000..ea8bc5bfbcf --- /dev/null +++ b/src/main/java/org/prebid/server/json/merge/JsonMergePatch.java @@ -0,0 +1,37 @@ +package org.prebid.server.json.merge; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializable; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.github.fge.jsonpatch.JsonPatchException; +import com.github.fge.jsonpatch.JsonPatchMessages; +import com.github.fge.jsonpatch.Patch; +import com.github.fge.msgsimple.bundle.MessageBundle; +import com.github.fge.msgsimple.load.MessageBundles; +import org.prebid.server.json.ObjectMapperProvider; + +import java.io.IOException; + +/** + * Json merge patch implementation that uses the application-wide object mapper. + * Replicates functionality from {@link com.github.fge.jsonpatch.mergepatch.JsonMergePatch}. + */ +@JsonDeserialize(using = JsonMergePatchDeserializer.class) +public abstract class JsonMergePatch implements JsonSerializable, Patch { + + private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper(); + public static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); + + public static JsonMergePatch fromJson(JsonNode node) throws JsonPatchException { + BUNDLE.checkNotNull(node, "jsonPatch.nullInput"); + try { + return MAPPER.readValue(node.traverse(), JsonMergePatch.class); + } catch (IOException e) { + throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.deserFailed"), e); + } + } + + @Override + public abstract JsonNode apply(JsonNode input) throws JsonPatchException; +} diff --git a/src/main/java/org/prebid/server/json/merge/JsonMergePatchDeserializer.java b/src/main/java/org/prebid/server/json/merge/JsonMergePatchDeserializer.java new file mode 100644 index 00000000000..33b2171ad48 --- /dev/null +++ b/src/main/java/org/prebid/server/json/merge/JsonMergePatchDeserializer.java @@ -0,0 +1,82 @@ +package org.prebid.server.json.merge; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import org.prebid.server.json.ObjectMapperProvider; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Replicates functionality from {@link com.github.fge.jsonpatch.mergepatch.JsonMergePatchDeserializer} + */ +final class JsonMergePatchDeserializer extends JsonDeserializer { + + /* + * FIXME! UGLY! HACK! + * + * We MUST have an ObjectCodec ready so that the parser in .deserialize() + * can actually do something useful -- for instance, deserializing even a + * JsonNode. + * + * Jackson does not do this automatically; I don't know why... + */ + private static final ObjectCodec CODEC = ObjectMapperProvider.mapper(); + + @Override + public JsonMergePatch deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException { + // FIXME: see comment above + jp.setCodec(CODEC); + final JsonNode node = jp.readValueAsTree(); + + /* + * Not an object: the simple case + */ + if (!node.isObject()) { + return new NonObjectMergePatch(node); + } + + /* + * The complicated case... + * + * We have to build a set of removed members, plus a map of modified + * members. + */ + + final Set removedMembers = new HashSet<>(); + final Map modifiedMembers = new HashMap<>(); + final Iterator> iterator = node.fields(); + + Map.Entry entry; + + while (iterator.hasNext()) { + entry = iterator.next(); + if (entry.getValue().isNull()) { + removedMembers.add(entry.getKey()); + } else { + final JsonMergePatch value = deserialize(entry.getValue().traverse(), ctxt); + modifiedMembers.put(entry.getKey(), value); + } + } + + return new ObjectMergePatch(removedMembers, modifiedMembers); + } + + /* + * This method MUST be overriden... The default is to return null, which is + * not what we want. + */ + @Override + @SuppressWarnings("deprecation") + public JsonMergePatch getNullValue() { + return new NonObjectMergePatch(NullNode.getInstance()); + } +} diff --git a/src/main/java/org/prebid/server/json/merge/NonObjectMergePatch.java b/src/main/java/org/prebid/server/json/merge/NonObjectMergePatch.java new file mode 100644 index 00000000000..4902a0c1548 --- /dev/null +++ b/src/main/java/org/prebid/server/json/merge/NonObjectMergePatch.java @@ -0,0 +1,42 @@ +package org.prebid.server.json.merge; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; + +import java.io.IOException; + +/** + * Replicates functionality from {@link com.github.fge.jsonpatch.mergepatch.NonObjectMergePatch} + */ +final class NonObjectMergePatch extends JsonMergePatch { + + private final JsonNode node; + + NonObjectMergePatch(final JsonNode node) { + if (node == null) { + throw new NullPointerException(); + } + this.node = node; + } + + @Override + public JsonNode apply(final JsonNode input) { + BUNDLE.checkNotNull(input, "jsonPatch.nullValue"); + return node; + } + + @Override + public void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeTree(node); + } + + @Override + public void serializeWithType(final JsonGenerator jgen, + final SerializerProvider provider, + final TypeSerializer typeSer) throws IOException { + + serialize(jgen, provider); + } +} diff --git a/src/main/java/org/prebid/server/json/merge/ObjectMergePatch.java b/src/main/java/org/prebid/server/json/merge/ObjectMergePatch.java new file mode 100644 index 00000000000..523709dbe29 --- /dev/null +++ b/src/main/java/org/prebid/server/json/merge/ObjectMergePatch.java @@ -0,0 +1,99 @@ +package org.prebid.server.json.merge; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.fge.jackson.JacksonUtils; +import com.github.fge.jsonpatch.JsonPatchException; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Replicates functionality from {@link com.github.fge.jsonpatch.mergepatch.ObjectMergePatch} + */ +final class ObjectMergePatch extends JsonMergePatch { + + private final Set removedMembers; + private final Map modifiedMembers; + + ObjectMergePatch(final Set removedMembers, final Map modifiedMembers) { + + this.removedMembers = Set.copyOf(removedMembers); + this.modifiedMembers = Map.copyOf(modifiedMembers); + } + + @Override + public JsonNode apply(final JsonNode input) + throws JsonPatchException { + BUNDLE.checkNotNull(input, "jsonPatch.nullValue"); + /* + * If the input is an object, we make a deep copy of it + */ + final ObjectNode ret = input.isObject() ? (ObjectNode) input.deepCopy() + : JacksonUtils.nodeFactory().objectNode(); + + /* + * Our result is now a JSON Object; first, add (or modify) existing + * members in the result + */ + String key; + JsonNode value; + for (final Map.Entry entry : modifiedMembers.entrySet()) { + + key = entry.getKey(); + /* + * FIXME: ugly... + * + * We treat missing keys as null nodes; this "works" because in + * the modifiedMembers map, values are JsonMergePatch instances: + * + * * if it is a NonObjectMergePatch, the value is replaced + * unconditionally; + * * if it is an ObjectMergePatch, we get back here; the value will + * be replaced with a JSON Object anyway before being processed. + */ + final JsonNode jsonNode = ret.get(key); + value = jsonNode != null ? jsonNode : NullNode.getInstance(); + ret.replace(key, entry.getValue().apply(value)); + } + + ret.remove(removedMembers); + + return ret; + } + + @Override + public void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeStartObject(); + + /* + * Write removed members as JSON nulls + */ + for (final String member : removedMembers) { + jgen.writeNullField(member); + } + + /* + * Write modified members; delegate to serialization for writing values + */ + for (final Map.Entry entry : modifiedMembers.entrySet()) { + jgen.writeFieldName(entry.getKey()); + entry.getValue().serialize(jgen, provider); + } + + jgen.writeEndObject(); + } + + @Override + public void serializeWithType(final JsonGenerator jgen, + final SerializerProvider provider, + final TypeSerializer typeSer) throws IOException { + + serialize(jgen, provider); + } +} 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..2ca42da4a45 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,9 +45,9 @@ 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()), + toOneLineString(routingContext.body().asString()), statusCode, responseBody); @@ -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/LoggerControlKnob.java b/src/main/java/org/prebid/server/log/LoggerControlKnob.java index 3f3c0932ffa..3c74cd36b15 100644 --- a/src/main/java/org/prebid/server/log/LoggerControlKnob.java +++ b/src/main/java/org/prebid/server/log/LoggerControlKnob.java @@ -19,7 +19,7 @@ public class LoggerControlKnob { private final Level originalLevel; private final Lock lock = new ReentrantLock(); - private Long restoreTimerId = null; + private Long restoreTimerId; public LoggerControlKnob(Vertx vertx) { this.vertx = Objects.requireNonNull(vertx); 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/AccountMetrics.java b/src/main/java/org/prebid/server/metric/AccountMetrics.java index f2cdb29f328..6bd479b7ea3 100644 --- a/src/main/java/org/prebid/server/metric/AccountMetrics.java +++ b/src/main/java/org/prebid/server/metric/AccountMetrics.java @@ -23,6 +23,7 @@ class AccountMetrics extends UpdatableMetrics { private final ResponseMetrics responseMetrics; private final HooksMetrics hooksMetrics; private final ActivitiesMetrics activitiesMetrics; + private final ProfileMetrics profileMetrics; AccountMetrics(MetricRegistry metricRegistry, CounterType counterType, String account) { super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), @@ -36,6 +37,7 @@ class AccountMetrics extends UpdatableMetrics { responseMetrics = new ResponseMetrics(metricRegistry, counterType, createPrefix(account)); hooksMetrics = new HooksMetrics(metricRegistry, counterType, createPrefix(account)); activitiesMetrics = new ActivitiesMetrics(metricRegistry, counterType, createPrefix(account)); + profileMetrics = new ProfileMetrics(metricRegistry, counterType, createPrefix(account)); } private static String createPrefix(String account) { @@ -73,4 +75,8 @@ HooksMetrics hooks() { ActivitiesMetrics activities() { return activitiesMetrics; } + + ProfileMetrics profiles() { + return profileMetrics; + } } diff --git a/src/main/java/org/prebid/server/metric/AlertsAccountConfigMetric.java b/src/main/java/org/prebid/server/metric/AlertsAccountConfigMetric.java index 4522b8e1bdf..a59aaa95306 100644 --- a/src/main/java/org/prebid/server/metric/AlertsAccountConfigMetric.java +++ b/src/main/java/org/prebid/server/metric/AlertsAccountConfigMetric.java @@ -14,4 +14,3 @@ private static Function nameCreator(String prefix, String ac return metricName -> "%s.account_config.%s.%s".formatted(prefix, account, metricName); } } - diff --git a/src/main/java/org/prebid/server/metric/CacheCreativeSizeMetrics.java b/src/main/java/org/prebid/server/metric/CacheCreativeSizeMetrics.java index 5791b370740..f0d56074944 100644 --- a/src/main/java/org/prebid/server/metric/CacheCreativeSizeMetrics.java +++ b/src/main/java/org/prebid/server/metric/CacheCreativeSizeMetrics.java @@ -1,18 +1,23 @@ package org.prebid.server.metric; import com.codahale.metrics.MetricRegistry; +import org.prebid.server.metric.model.CacheCreativeType; import java.util.Objects; import java.util.function.Function; public class CacheCreativeSizeMetrics extends UpdatableMetrics { - CacheCreativeSizeMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + CacheCreativeSizeMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String prefix, + CacheCreativeType type) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), - nameCreator(Objects.requireNonNull(prefix))); + nameCreator(Objects.requireNonNull(prefix), Objects.requireNonNull(type))); } - private static Function nameCreator(String prefix) { - return metricName -> "%s.creative_size.%s".formatted(prefix, metricName); + private static Function nameCreator(String prefix, CacheCreativeType type) { + return metricName -> "%s.%s_size.%s".formatted(prefix, type.getType(), metricName); } } diff --git a/src/main/java/org/prebid/server/metric/CacheCreativeTtlMetrics.java b/src/main/java/org/prebid/server/metric/CacheCreativeTtlMetrics.java new file mode 100644 index 00000000000..f79ce39f96a --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheCreativeTtlMetrics.java @@ -0,0 +1,24 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.prebid.server.metric.model.CacheCreativeType; + +import java.util.Objects; +import java.util.function.Function; + +public class CacheCreativeTtlMetrics extends UpdatableMetrics { + + CacheCreativeTtlMetrics(MetricRegistry metricRegistry, + CounterType counterType, + String prefix, + CacheCreativeType type) { + + super(Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix), Objects.requireNonNull(type))); + } + + private static Function nameCreator(String prefix, CacheCreativeType type) { + return metricName -> "%s.%s_ttl.%s".formatted(prefix, type.getType(), metricName); + } +} diff --git a/src/main/java/org/prebid/server/metric/CacheMetrics.java b/src/main/java/org/prebid/server/metric/CacheMetrics.java index 23e1a2ea8d8..6719bc995d2 100644 --- a/src/main/java/org/prebid/server/metric/CacheMetrics.java +++ b/src/main/java/org/prebid/server/metric/CacheMetrics.java @@ -1,7 +1,10 @@ package org.prebid.server.metric; import com.codahale.metrics.MetricRegistry; +import org.prebid.server.metric.model.CacheCreativeType; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.Function; @@ -12,6 +15,10 @@ class CacheMetrics extends UpdatableMetrics { private final RequestMetrics requestsMetrics; private final CacheCreativeSizeMetrics cacheCreativeSizeMetrics; + private final CacheCreativeTtlMetrics cacheCreativeTtlMetrics; + private final CacheVtrackMetrics cacheVtrackMetrics; + private final Map cacheModuleStorageMetrics; + private final Function cacheModuleStorageMetricsCreator; CacheMetrics(MetricRegistry metricRegistry, CounterType counterType) { super( @@ -20,7 +27,14 @@ class CacheMetrics extends UpdatableMetrics { nameCreator(createPrefix())); requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix()); - cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics(metricRegistry, counterType, createPrefix()); + cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics( + metricRegistry, counterType, createPrefix(), CacheCreativeType.CREATIVE); + cacheCreativeTtlMetrics = new CacheCreativeTtlMetrics( + metricRegistry, counterType, createPrefix(), CacheCreativeType.CREATIVE); + cacheVtrackMetrics = new CacheVtrackMetrics(metricRegistry, counterType, createPrefix()); + cacheModuleStorageMetrics = new HashMap<>(); + cacheModuleStorageMetricsCreator = moduleCode -> + new CacheModuleStorageMetrics(metricRegistry, counterType, createPrefix(), moduleCode); } CacheMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { @@ -30,7 +44,14 @@ class CacheMetrics extends UpdatableMetrics { nameCreator(createPrefix(Objects.requireNonNull(prefix)))); requestsMetrics = new RequestMetrics(metricRegistry, counterType, createPrefix(prefix)); - cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics(metricRegistry, counterType, createPrefix(prefix)); + cacheCreativeSizeMetrics = new CacheCreativeSizeMetrics( + metricRegistry, counterType, createPrefix(prefix), CacheCreativeType.CREATIVE); + cacheCreativeTtlMetrics = new CacheCreativeTtlMetrics( + metricRegistry, counterType, createPrefix(prefix), CacheCreativeType.CREATIVE); + cacheVtrackMetrics = new CacheVtrackMetrics(metricRegistry, counterType, createPrefix(prefix)); + cacheModuleStorageMetrics = new HashMap<>(); + cacheModuleStorageMetricsCreator = moduleCode -> + new CacheModuleStorageMetrics(metricRegistry, counterType, createPrefix(), moduleCode); } private static String createPrefix(String prefix) { @@ -52,4 +73,16 @@ RequestMetrics requests() { CacheCreativeSizeMetrics creativeSize() { return cacheCreativeSizeMetrics; } + + CacheCreativeTtlMetrics creativeTtl() { + return cacheCreativeTtlMetrics; + } + + CacheVtrackMetrics vtrack() { + return cacheVtrackMetrics; + } + + CacheModuleStorageMetrics moduleStorage(String moduleCode) { + return cacheModuleStorageMetrics.computeIfAbsent(moduleCode, cacheModuleStorageMetricsCreator); + } } diff --git a/src/main/java/org/prebid/server/metric/CacheModuleStorageMetrics.java b/src/main/java/org/prebid/server/metric/CacheModuleStorageMetrics.java new file mode 100644 index 00000000000..5bc52b503cb --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheModuleStorageMetrics.java @@ -0,0 +1,54 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.prebid.server.metric.model.CacheCreativeType; + +import java.util.Objects; +import java.util.function.Function; + +class CacheModuleStorageMetrics extends UpdatableMetrics { + + private final CacheReadMetrics readMetrics; + private final CacheWriteMetrics writeMetrics; + private final CacheCreativeSizeMetrics entrySizeMetrics; + private final CacheCreativeTtlMetrics entryTtlMetrics; + + CacheModuleStorageMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix, String module) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix), Objects.requireNonNull(module)))); + + readMetrics = new CacheReadMetrics(metricRegistry, counterType, createPrefix(prefix, module)); + writeMetrics = new CacheWriteMetrics(metricRegistry, counterType, createPrefix(prefix, module)); + entrySizeMetrics = new CacheCreativeSizeMetrics( + metricRegistry, counterType, createPrefix(prefix, module), CacheCreativeType.ENTRY); + entryTtlMetrics = new CacheCreativeTtlMetrics( + metricRegistry, counterType, createPrefix(prefix, module), CacheCreativeType.ENTRY); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.%s".formatted(prefix, metricName); + } + + private static String createPrefix(String prefix, String moduleCode) { + return "%s.module_storage.%s".formatted(prefix, moduleCode); + } + + CacheReadMetrics read() { + return readMetrics; + } + + CacheWriteMetrics write() { + return writeMetrics; + } + + CacheCreativeSizeMetrics entrySize() { + return entrySizeMetrics; + } + + CacheCreativeTtlMetrics entryTtl() { + return entryTtlMetrics; + } + +} diff --git a/src/main/java/org/prebid/server/metric/CacheReadMetrics.java b/src/main/java/org/prebid/server/metric/CacheReadMetrics.java new file mode 100644 index 00000000000..356d96fb9d1 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheReadMetrics.java @@ -0,0 +1,20 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +public class CacheReadMetrics extends UpdatableMetrics { + + CacheReadMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.read.%s".formatted(prefix, metricName); + } + +} diff --git a/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java b/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java new file mode 100644 index 00000000000..e3a44639297 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheVtrackMetrics.java @@ -0,0 +1,54 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; +import org.prebid.server.metric.model.CacheCreativeType; + +import java.util.Objects; +import java.util.function.Function; + +class CacheVtrackMetrics extends UpdatableMetrics { + + private final CacheReadMetrics readMetrics; + private final CacheWriteMetrics writeMetrics; + private final CacheCreativeSizeMetrics creativeSizeMetrics; + private final CacheCreativeTtlMetrics creativeTtlMetrics; + + CacheVtrackMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(createPrefix(Objects.requireNonNull(prefix)))); + + readMetrics = new CacheReadMetrics(metricRegistry, counterType, createPrefix(prefix)); + writeMetrics = new CacheWriteMetrics(metricRegistry, counterType, createPrefix(prefix)); + creativeSizeMetrics = new CacheCreativeSizeMetrics( + metricRegistry, counterType, createPrefix(prefix), CacheCreativeType.CREATIVE); + creativeTtlMetrics = new CacheCreativeTtlMetrics( + metricRegistry, counterType, createPrefix(prefix), CacheCreativeType.CREATIVE); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.%s".formatted(prefix, metricName); + } + + private static String createPrefix(String prefix) { + return prefix + ".vtrack"; + } + + CacheReadMetrics read() { + return readMetrics; + } + + CacheWriteMetrics write() { + return writeMetrics; + } + + CacheCreativeSizeMetrics creativeSize() { + return creativeSizeMetrics; + } + + CacheCreativeTtlMetrics creativeTtl() { + return creativeTtlMetrics; + } + +} diff --git a/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java b/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java new file mode 100644 index 00000000000..d0d598d6ae5 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/CacheWriteMetrics.java @@ -0,0 +1,19 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +public class CacheWriteMetrics extends UpdatableMetrics { + + CacheWriteMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super(Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.write.%s".formatted(prefix, metricName); + } +} diff --git a/src/main/java/org/prebid/server/metric/CookieSyncMetrics.java b/src/main/java/org/prebid/server/metric/CookieSyncMetrics.java index 6d3120343e8..eb4b58a9336 100644 --- a/src/main/java/org/prebid/server/metric/CookieSyncMetrics.java +++ b/src/main/java/org/prebid/server/metric/CookieSyncMetrics.java @@ -49,4 +49,3 @@ private static Function nameCreator(String prefix) { } } } - diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java index 09467050ff7..2f0950c5ec3 100644 --- a/src/main/java/org/prebid/server/metric/MetricName.java +++ b/src/main/java/org/prebid/server/metric/MetricName.java @@ -25,11 +25,13 @@ public enum MetricName { // auction requests, + debug_requests, app_requests, no_cookie_requests, request_time, prices, imps_requested, + imps_dropped, imps_banner, imps_video, imps_native, @@ -62,8 +64,10 @@ public enum MetricName { nobid, gotbids, badinput, - blacklisted_account, - blacklisted_app, + disabled_bidder, + unknown_bidder, + blocklisted_account, + blocklisted_app, badserverresponse, failedtorequestbids, timeout, @@ -71,6 +75,8 @@ public enum MetricName { unknown_error, err, networkerr, + buyeruid_scrubbed, + seat, // bids validation warn, @@ -115,6 +121,7 @@ public enum MetricName { // cache creative types json, xml, + text, // account.*.requests. rejected_by_invalid_account("rejected.invalid-account"), @@ -138,6 +145,7 @@ public enum MetricName { call, success, noop, + no_invocation("no-invocation"), reject, unknown, failure, @@ -147,35 +155,12 @@ 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"); + processed_rules_count("processedrules.count"), + + // profiles + limit_exceeded; private final String name; diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java index 8ce8e1b4936..e7b290bc14a 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,7 @@ public class Metrics extends UpdatableMetrics { private final CurrencyRatesMetrics currencyRatesMetrics; private final Map settingsCacheMetrics; private final HooksMetrics hooksMetrics; - private final PgMetrics pgMetrics; + private final ProfileMetrics profileMetrics; public Metrics(MetricRegistry metricRegistry, CounterType counterType, @@ -97,7 +98,7 @@ public Metrics(MetricRegistry metricRegistry, currencyRatesMetrics = new CurrencyRatesMetrics(metricRegistry, counterType); settingsCacheMetrics = new HashMap<>(); hooksMetrics = new HooksMetrics(metricRegistry, counterType); - pgMetrics = new PgMetrics(metricRegistry, counterType); + profileMetrics = new ProfileMetrics(metricRegistry, counterType); } RequestsMetrics requests() { @@ -140,10 +141,6 @@ UserSyncMetrics userSync() { return userSyncMetrics; } - PgMetrics pgMetrics() { - return pgMetrics; - } - CookieSyncMetrics cookieSync() { return cookieSyncMetrics; } @@ -172,6 +169,12 @@ HooksMetrics hooks() { return hooksMetrics; } + public void updateDebugRequestMetrics(boolean debugEnabled) { + if (debugEnabled) { + incCounter(MetricName.debug_requests); + } + } + public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) { if (isApp) { incCounter(MetricName.app_requests); @@ -181,6 +184,10 @@ public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean l incCounter(MetricName.imps_requested, numImps); } + public void updateImpsDroppedMetric(int numImps) { + incCounter(MetricName.imps_dropped, numImps); + } + public void updateImpTypesMetrics(List imps) { final Map mediaTypeToCount = imps.stream() @@ -240,12 +247,20 @@ public void updateAccountRequestMetrics(Account account, MetricName requestType) final AccountMetrics accountMetrics = forAccount(account.getId()); accountMetrics.incCounter(MetricName.requests); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed)) { accountMetrics.requestType(requestType).incCounter(MetricName.requests); } } } + public void updateAccountDebugRequestMetrics(Account account, boolean debugEnabled) { + final AccountMetricsVerbosityLevel verbosityLevel = accountMetricsVerbosityResolver.forAccount(account); + if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed) && debugEnabled) { + forAccount(account.getId()).incCounter(MetricName.debug_requests); + } + } + public void updateAccountRequestRejectedByInvalidAccountMetrics(String accountId) { updateAccountRequestsMetrics(accountId, MetricName.rejected_by_invalid_account); } @@ -276,6 +291,13 @@ public void updateAdapterRequestTypeAndNoCookieMetrics(String bidder, MetricName } } + public void updateAdapterRequestBuyerUidScrubbedMetrics(String bidder, Account account) { + forAdapter(bidder).request().incCounter(MetricName.buyeruid_scrubbed); + if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + forAccount(account.getId()).adapter().forAdapter(bidder).request().incCounter(MetricName.buyeruid_scrubbed); + } + } + public void updateAdapterResponseTime(String bidder, Account account, int responseTime) { final AdapterTypeMetrics adapterTypeMetrics = forAdapter(bidder); adapterTypeMetrics.updateTimer(MetricName.request_time, responseTime); @@ -320,6 +342,22 @@ public void updateAdapterRequestErrorMetric(String bidder, MetricName errorMetri forAdapter(bidder).request().incCounter(errorMetric); } + public void updateDisabledBidderMetric(Account account) { + incCounter(MetricName.disabled_bidder); + if (accountMetricsVerbosityResolver.forAccount(account) + .isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + forAccount(account.getId()).requests().incCounter(MetricName.disabled_bidder); + } + } + + public void updateUnknownBidderMetric(Account account) { + incCounter(MetricName.unknown_bidder); + if (accountMetricsVerbosityResolver.forAccount(account) + .isAtLeast(AccountMetricsVerbosityLevel.detailed)) { + forAccount(account.getId()).requests().incCounter(MetricName.unknown_bidder); + } + } + public void updateAnalyticEventMetric(String analyticCode, MetricName eventType, MetricName result) { forAnalyticReporter(analyticCode).forEventType(eventType).incCounter(result); } @@ -350,6 +388,10 @@ public void updateSecureValidationMetrics(String bidder, String accountId, Metri forAccount(accountId).response().validation().secure().incCounter(type); } + public void updateSeatValidationMetrics(String bidder) { + forAdapter(bidder).response().validation().incCounter(MetricName.seat); + } + public void updateUserSyncOptoutMetric() { userSync().incCounter(MetricName.opt_outs); } @@ -394,47 +436,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 (userIdsRemoved) { - tcf.incCounter(MetricName.userid_removed); + + if (userFpdRemoved || lmtEnabled) { + activityInfrastructure.updateActivityMetrics(Activity.TRANSMIT_UFPD, ComponentType.BIDDER, bidder); + if (userFpdRemoved) { + tcf.incCounter(MetricName.userfpd_masked); + } + } + 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 +582,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) { @@ -591,14 +612,44 @@ public void updateStoredImpsMetric(boolean found) { } } - public void updateCacheRequestSuccessTime(String accountId, long timeElapsed) { - cache().requests().updateTimer(MetricName.ok, timeElapsed); - forAccount(accountId).cache().requests().updateTimer(MetricName.ok, timeElapsed); + public void updateVtrackCacheReadRequestTime(long timeElapsed, MetricName metricName) { + cache().vtrack().read().updateTimer(metricName, timeElapsed); + } + + public void updateVtrackCacheWriteRequestTime(String accountId, long timeElapsed, MetricName metricName) { + cache().vtrack().write().updateTimer(metricName, timeElapsed); + forAccount(accountId).cache().vtrack().write().updateTimer(metricName, timeElapsed); + } + + public void updateVtrackCacheCreativeSize(String accountId, int creativeSize, MetricName creativeType) { + cache().vtrack().creativeSize().updateHistogram(creativeType, creativeSize); + forAccount(accountId).cache().vtrack().creativeSize().updateHistogram(creativeType, creativeSize); + } + + public void updateVtrackCacheCreativeTtl(String accountId, Integer creativeTtl, MetricName creativeType) { + cache().vtrack().creativeTtl().updateHistogram(creativeType, creativeTtl); + forAccount(accountId).cache().vtrack().creativeTtl().updateHistogram(creativeType, creativeTtl); } - public void updateCacheRequestFailedTime(String accountId, long timeElapsed) { - cache().requests().updateTimer(MetricName.err, timeElapsed); - forAccount(accountId).cache().requests().updateTimer(MetricName.err, timeElapsed); + public void updateModuleStorageCacheReadRequestTime(String moduleCode, long timeElapsed, MetricName metricName) { + cache().moduleStorage(moduleCode).read().updateTimer(metricName, timeElapsed); + } + + public void updateModuleStorageCacheWriteRequestTime(String moduleCode, long timeElapsed, MetricName metricName) { + cache().moduleStorage(moduleCode).write().updateTimer(metricName, timeElapsed); + } + + public void updateModuleStorageCacheEntrySize(String moduleCode, int entrySize, MetricName type) { + cache().moduleStorage(moduleCode).entrySize().updateHistogram(type, entrySize); + } + + public void updateModuleStorageCacheEntryTtl(String moduleCode, Integer entryTtl, MetricName type) { + cache().moduleStorage(moduleCode).entryTtl().updateHistogram(type, entryTtl); + } + + public void updateAuctionCacheRequestTime(String accountId, long timeElapsed, MetricName metricName) { + cache().requests().updateTimer(metricName, timeElapsed); + forAccount(accountId).cache().requests().updateTimer(metricName, timeElapsed); } public void updateCacheCreativeSize(String accountId, int creativeSize, MetricName creativeType) { @@ -606,6 +657,11 @@ public void updateCacheCreativeSize(String accountId, int creativeSize, MetricNa forAccount(accountId).cache().creativeSize().updateHistogram(creativeType, creativeSize); } + public void updateCacheCreativeTtl(String accountId, Integer creativeTtl, MetricName creativeType) { + cache().creativeTtl().updateHistogram(creativeType, creativeTtl); + forAccount(accountId).cache().creativeTtl().updateHistogram(creativeType, creativeTtl); + } + public void updateTimeoutNotificationMetric(boolean success) { if (success) { timeoutNotificationMetrics.incCounter(MetricName.ok); @@ -640,13 +696,20 @@ public void updateHooksMetrics( final HookImplMetrics hookImplMetrics = hooks().module(moduleCode).stage(stage).hookImpl(hookImplCode); - hookImplMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { hookImplMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { hookImplMetrics.incCounter(HookMetricMapper.fromStatus(status)); } - hookImplMetrics.updateTimer(MetricName.duration, executionTime); + + if (action != ExecutionAction.no_invocation) { + hookImplMetrics.updateTimer(MetricName.duration, executionTime); + } + } public void updateAccountHooksMetrics( @@ -658,7 +721,10 @@ public void updateAccountHooksMetrics( if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) { final ModuleMetrics accountModuleMetrics = forAccount(account.getId()).hooks().module(moduleCode); - accountModuleMetrics.incCounter(MetricName.call); + if (action != ExecutionAction.no_invocation) { + accountModuleMetrics.incCounter(MetricName.call); + } + if (status == ExecutionStatus.success) { accountModuleMetrics.success().incCounter(HookMetricMapper.fromAction(action)); } else { @@ -673,6 +739,34 @@ public void updateAccountModuleDurationMetric(Account account, String moduleCode } } + public void updateRequestsActivityDisallowedCount(Activity activity) { + requests().activities().forActivity(activity).incCounter(MetricName.disallowed_count); + } + + public void updateAccountActivityDisallowedCount(String account, Activity activity) { + forAccount(account).activities().forActivity(activity).incCounter(MetricName.disallowed_count); + } + + public void updateAdapterActivityDisallowedCount(String adapter, Activity activity) { + forAdapter(adapter).activities().forActivity(activity).incCounter(MetricName.disallowed_count); + } + + public void updateRequestsActivityProcessedRulesCount() { + requests().activities().incCounter(MetricName.processed_rules_count); + } + + public void updateAccountActivityProcessedRulesCount(String account) { + forAccount(account).activities().incCounter(MetricName.processed_rules_count); + } + + public void updateProfileMetric(MetricName metricName) { + profileMetrics.incCounter(metricName); + } + + public void updateAccountProfileMetric(String account, MetricName metricName) { + forAccount(account).profiles().incCounter(metricName); + } + private static class HookMetricMapper { private static final EnumMap STATUS_TO_METRIC = @@ -689,6 +783,7 @@ private static class HookMetricMapper { ACTION_TO_METRIC.put(ExecutionAction.no_action, MetricName.noop); ACTION_TO_METRIC.put(ExecutionAction.update, MetricName.update); ACTION_TO_METRIC.put(ExecutionAction.reject, MetricName.reject); + ACTION_TO_METRIC.put(ExecutionAction.no_invocation, MetricName.no_invocation); } static MetricName fromStatus(ExecutionStatus status) { @@ -699,36 +794,4 @@ static MetricName fromAction(ExecutionAction action) { return ACTION_TO_METRIC.getOrDefault(action, MetricName.unknown); } } - - 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); - } - - public void updateAccountActivityDisallowedCount(String account, Activity activity) { - forAccount(account).activities().forActivity(activity).incCounter(MetricName.disallowed_count); - } - - public void updateAdapterActivityDisallowedCount(String adapter, Activity activity) { - forAdapter(adapter).activities().forActivity(activity).incCounter(MetricName.disallowed_count); - } - - public void updateRequestsActivityProcessedRulesCount() { - requests().activities().incCounter(MetricName.processed_rules_count); - } - - public void updateAccountActivityProcessedRulesCount(String account) { - forAccount(account).activities().incCounter(MetricName.processed_rules_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/metric/ProfileMetrics.java b/src/main/java/org/prebid/server/metric/ProfileMetrics.java new file mode 100644 index 00000000000..3601938b1b1 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/ProfileMetrics.java @@ -0,0 +1,28 @@ +package org.prebid.server.metric; + +import com.codahale.metrics.MetricRegistry; + +import java.util.Objects; +import java.util.function.Function; + +class ProfileMetrics extends UpdatableMetrics { + + ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType) { + super(Objects.requireNonNull(metricRegistry), Objects.requireNonNull(counterType), nameCreator()); + } + + ProfileMetrics(MetricRegistry metricRegistry, CounterType counterType, String prefix) { + super( + Objects.requireNonNull(metricRegistry), + Objects.requireNonNull(counterType), + nameCreator(Objects.requireNonNull(prefix))); + } + + private static Function nameCreator() { + return "profiles.%s"::formatted; + } + + private static Function nameCreator(String prefix) { + return metricName -> "%s.profiles.%s".formatted(prefix, metricName); + } +} diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java index 1cc8f3adfb3..025a47368bd 100644 --- a/src/main/java/org/prebid/server/metric/StageMetrics.java +++ b/src/main/java/org/prebid/server/metric/StageMetrics.java @@ -21,6 +21,8 @@ class StageMetrics extends UpdatableMetrics { STAGE_TO_METRIC.put(Stage.raw_bidder_response, "rawbidresponse"); STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse"); STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse"); + STAGE_TO_METRIC.put(Stage.all_processed_bid_responses, "allprocbidresponses"); + STAGE_TO_METRIC.put(Stage.exitpoint, "exitpoint"); } private static final String UNKNOWN_STAGE = "unknown"; diff --git a/src/main/java/org/prebid/server/metric/model/CacheCreativeType.java b/src/main/java/org/prebid/server/metric/model/CacheCreativeType.java new file mode 100644 index 00000000000..cd7b8ec78e2 --- /dev/null +++ b/src/main/java/org/prebid/server/metric/model/CacheCreativeType.java @@ -0,0 +1,17 @@ +package org.prebid.server.metric.model; + +public enum CacheCreativeType { + + ENTRY("entry"), + CREATIVE("creative"); + + private final String type; + + CacheCreativeType(String type) { + this.type = type; + } + + public String getType() { + return this.type; + } +} diff --git a/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java b/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java index b44590db397..5f2a4c61897 100644 --- a/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java +++ b/src/main/java/org/prebid/server/model/CaseInsensitiveMultiMap.java @@ -11,7 +11,7 @@ public class CaseInsensitiveMultiMap { private static final CaseInsensitiveMultiMap EMPTY = builder().build(); - private final io.vertx.core.MultiMap delegate; + private final MultiMap delegate; private CaseInsensitiveMultiMap(MultiMap delegate) { this.delegate = delegate; @@ -85,10 +85,10 @@ public int hashCode() { public static class Builder { - private final io.vertx.core.MultiMap delegate; + private final MultiMap delegate; public Builder() { - this.delegate = io.vertx.core.MultiMap.caseInsensitiveMultiMap(); + this.delegate = MultiMap.caseInsensitiveMultiMap(); } public CaseInsensitiveMultiMap build() { diff --git a/src/main/java/org/prebid/server/model/HttpRequestContext.java b/src/main/java/org/prebid/server/model/HttpRequestContext.java index efa07ed621e..9237e9b1803 100644 --- a/src/main/java/org/prebid/server/model/HttpRequestContext.java +++ b/src/main/java/org/prebid/server/model/HttpRequestContext.java @@ -2,6 +2,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; import io.vertx.ext.web.RoutingContext; import lombok.Builder; import lombok.Value; @@ -16,6 +17,8 @@ @Value public class HttpRequestContext { + HttpMethod httpMethod; + String absoluteUri; CaseInsensitiveMultiMap queryParams; @@ -30,6 +33,7 @@ public class HttpRequestContext { public static HttpRequestContext from(RoutingContext context) { return HttpRequestContext.builder() + .httpMethod(context.request().method()) .absoluteUri(context.request().uri()) .queryParams(CaseInsensitiveMultiMap.builder().addAll(toMap(context.request().params())).build()) .headers(headers(context)) diff --git a/src/main/java/org/prebid/server/model/UpdateResult.java b/src/main/java/org/prebid/server/model/UpdateResult.java index 3c28be53ebe..a96275d0e42 100644 --- a/src/main/java/org/prebid/server/model/UpdateResult.java +++ b/src/main/java/org/prebid/server/model/UpdateResult.java @@ -2,7 +2,7 @@ import lombok.Value; -@Value +@Value(staticConstructor = "of") public class UpdateResult { boolean updated; 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..1588f1827af 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; @@ -51,10 +51,10 @@ private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse tcfRe return HostVendorTcfResponse.of( tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(), - isCookieSyncAllowed(tcfResponse)); + isVendorAllowed(tcfResponse)); } - private boolean isCookieSyncAllowed(TcfResponse hostTcfResponse) { + private boolean isVendorAllowed(TcfResponse hostTcfResponse) { return Optional.ofNullable(hostTcfResponse.getActions()) .map(vendorIdToAction -> vendorIdToAction.get(gdprHostVendorId)) .map(hostActions -> !hostActions.isBlockPixelSync()) diff --git a/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java b/src/main/java/org/prebid/server/privacy/PrivacyExtractor.java index 5f2e6df2025..aa97f3d164e 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; @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * GDPR-aware utilities @@ -110,10 +111,10 @@ public Privacy toValidPrivacy(String gdpr, final String validGdpr = ObjectUtils.notEqual(gdpr, "1") && ObjectUtils.notEqual(gdpr, "0") ? DEFAULT_GDPR_VALUE : gdpr; - final String validConsent = StringUtils.defaultString(consent, DEFAULT_CONSENT_VALUE); + final String validConsent = Objects.toString(consent, DEFAULT_CONSENT_VALUE); final Ccpa validCcpa = usPrivacy == null ? DEFAULT_CCPA_VALUE : toValidCcpa(usPrivacy, errors); final Integer validCoppa = coppa == null ? DEFAULT_COPPA_VALUE : coppa; - final String validGpp = StringUtils.defaultString(gpp, DEFAULT_GPP_VALUE); + final String validGpp = Objects.toString(gpp, DEFAULT_GPP_VALUE); final List validGppSid = ListUtils.defaultIfNull(gppSid, DEFAULT_GPP_SID_VALUE); return Privacy.builder() 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..0d86272d378 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.execution.timeout.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.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; @@ -41,15 +44,15 @@ public class TcfDefinerService { private static final Logger logger = LoggerFactory.getLogger(TcfDefinerService.class); - private static final ConditionalLogger AMP_CORRUPT_CONSENT_LOGGER = + private static final ConditionalLogger ampCorruptConsentLogger = new ConditionalLogger("amp_corrupt_consent", logger); - private static final ConditionalLogger APP_CORRUPT_CONSENT_LOGGER = + private static final ConditionalLogger appCorruptConsentLogger = new ConditionalLogger("app_corrupt_consent", logger); - private static final ConditionalLogger SITE_CORRUPT_CONSENT_LOGGER = + private static final ConditionalLogger siteCorruptConsentLogger = new ConditionalLogger("site_corrupt_consent", logger); - private static final ConditionalLogger DOOH_CORRUPT_CONSENT_LOGGER = + private static final ConditionalLogger doohCorruptConsentLogger = new ConditionalLogger("dooh_corrupt_consent", logger); - private static final ConditionalLogger UNDEFINED_CORRUPT_CONSENT_LOGGER = + private static final ConditionalLogger undefinedCorruptConsentLogger = new ConditionalLogger("undefined_corrupt_consent", logger); private static final String GDPR_ENABLED = "1"; @@ -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); + undefinedCorruptConsentLogger.warn(message, samplingRate); + warnings.add(message); } return TCStringParsingResult.of(tcString, warnings); @@ -373,20 +390,20 @@ private static void logWarn(String consent, String message, RequestLogInfo reque if (requestLogInfo == null || requestLogInfo.getRequestType() == null) { final String exceptionMessage = "Parsing consent string:\"%s\" failed for undefined type with exception %s" .formatted(consent, message); - UNDEFINED_CORRUPT_CONSENT_LOGGER.info(exceptionMessage, 100); + undefinedCorruptConsentLogger.info(exceptionMessage, 100); return; } switch (requestLogInfo.getRequestType()) { - case amp -> AMP_CORRUPT_CONSENT_LOGGER.info( + case amp -> ampCorruptConsentLogger.info( logMessage(consent, MetricName.amp.toString(), requestLogInfo, message), 100); - case openrtb2app -> APP_CORRUPT_CONSENT_LOGGER.info( + case openrtb2app -> appCorruptConsentLogger.info( logMessage(consent, MetricName.openrtb2app.toString(), requestLogInfo, message), 100); - case openrtb2dooh -> DOOH_CORRUPT_CONSENT_LOGGER.info( + case openrtb2dooh -> doohCorruptConsentLogger.info( logMessage(consent, MetricName.openrtb2dooh.toString(), requestLogInfo, message), 100); - case openrtb2web -> SITE_CORRUPT_CONSENT_LOGGER.info( + case openrtb2web -> siteCorruptConsentLogger.info( logMessage(consent, MetricName.openrtb2web.toString(), requestLogInfo, message), 100); - default -> UNDEFINED_CORRUPT_CONSENT_LOGGER.info( + default -> undefinedCorruptConsentLogger.info( logMessage(consent, "video or sync or setuid", requestLogInfo, message), 100); } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java index 3076d653030..d8d360fa823 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java @@ -1,35 +1,25 @@ package org.prebid.server.privacy.gdpr; -import org.prebid.server.auction.BidderAliases; +import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.bidder.BidderCatalog; public class VendorIdResolver { private final BidderAliases aliases; - private final BidderCatalog bidderCatalog; - private VendorIdResolver(BidderAliases aliases, BidderCatalog bidderCatalog) { + private VendorIdResolver(BidderAliases aliases) { this.aliases = aliases; - this.bidderCatalog = bidderCatalog; } - public static VendorIdResolver of(BidderAliases aliases, BidderCatalog bidderCatalog) { - return new VendorIdResolver(aliases, bidderCatalog); + public static VendorIdResolver of(BidderAliases aliases) { + return new VendorIdResolver(aliases); } public static VendorIdResolver of(BidderCatalog bidderCatalog) { - return of(null, bidderCatalog); + return of(BidderAliases.of(null, null, bidderCatalog)); } public Integer resolve(String aliasOrBidder) { - final Integer requestAliasVendorId = aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null; - - return requestAliasVendorId != null ? requestAliasVendorId : resolveViaCatalog(aliasOrBidder); - } - - private Integer resolveViaCatalog(String aliasOrBidder) { - final String bidderName = aliases != null ? aliases.resolveBidder(aliasOrBidder) : aliasOrBidder; - - return bidderCatalog.isActive(bidderName) ? bidderCatalog.vendorIdByName(bidderName) : null; + return aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null; } } diff --git a/src/main/java/org/prebid/server/privacy/gdpr/model/VendorPermission.java b/src/main/java/org/prebid/server/privacy/gdpr/model/VendorPermission.java index 3686d2128b3..0eca0ed1105 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/model/VendorPermission.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/model/VendorPermission.java @@ -27,4 +27,3 @@ public void consentNaturallyWith(PurposeCode purposeCode) { naturallyConsentedPurposes.add(purposeCode); } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose01Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose01Strategy.java index d09d864ac3f..226c1910d24 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose01Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose01Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.ONE; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose02Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose02Strategy.java index e85d912f3b8..8fad704d62a 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose02Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose02Strategy.java @@ -30,4 +30,3 @@ public PurposeCode getPurpose() { return PurposeCode.TWO; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose03Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose03Strategy.java index 4f93951e49c..b6251b75af5 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose03Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose03Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.THREE; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose04Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose04Strategy.java index b6e14d87a79..5c2cf863b46 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose04Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose04Strategy.java @@ -33,4 +33,3 @@ public PurposeCode getPurpose() { return PurposeCode.FOUR; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose05Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose05Strategy.java index f4258c88068..30913072c83 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose05Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose05Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.FIVE; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose06Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose06Strategy.java index e79bc1b0893..50945cc7b87 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose06Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose06Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.SIX; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose07Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose07Strategy.java index 0cd4b1784cb..8e388c043c9 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose07Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose07Strategy.java @@ -31,4 +31,3 @@ public PurposeCode getPurpose() { } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose08Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose08Strategy.java index 1c101aa753b..8c084fa9d48 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose08Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose08Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.EIGHT; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose09Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose09Strategy.java index 7e77e9dc24e..3a2704b595b 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose09Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose09Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.NINE; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose10Strategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose10Strategy.java index 06a42c090fa..458cd187bd4 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose10Strategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/Purpose10Strategy.java @@ -29,4 +29,3 @@ public PurposeCode getPurpose() { return PurposeCode.TEN; } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java index 322f7c4fc7a..e9e83a3ed9c 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/tcfstrategies/purpose/PurposeStrategy.java @@ -47,7 +47,7 @@ public void allow(VendorPermission vendorPermission) { /** * This method represents allowance of permission that purpose should provide after full enforcement - * (can downgrade to basic if GVL failed) despite of host company or account configuration. + * (can downgrade to basic if GVL failed) despite host company or account configuration. */ protected abstract void allowNaturally(PrivacyEnforcementAction privacyEnforcementAction); @@ -136,4 +136,3 @@ private Stream allowedByFullTypeStrategy( getPurpose(), vendorConsent, vendorForPurpose, excludedVendors, isEnforceVendors); } } - 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..51c9b3e9252 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,9 +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; import org.apache.commons.lang3.StringUtils; @@ -19,11 +16,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 +152,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 +345,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); @@ -396,8 +395,7 @@ private void stopUsingFallbackForVersion(int version) { versionsToFallback.remove(version); } - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class VendorListResult { int 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/privacy/gdpr/vendorlist/proto/Vendor.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Vendor.java index c2bf0a12964..6bb2be9dddb 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Vendor.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/Vendor.java @@ -45,4 +45,3 @@ public static Vendor empty(Integer id) { .build(); } } - diff --git a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorList.java b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorList.java index d6451bae6b7..d3ec53137dc 100644 --- a/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorList.java +++ b/src/main/java/org/prebid/server/privacy/gdpr/vendorlist/proto/VendorList.java @@ -1,14 +1,12 @@ package org.prebid.server.privacy.gdpr.vendorlist.proto; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Date; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class VendorList { @JsonProperty("vendorListVersion") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtIncludeBrandCategory.java b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtIncludeBrandCategory.java index 0c61a578986..bc004442fff 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtIncludeBrandCategory.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtIncludeBrandCategory.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.ext.prebid.targeting.includebrandcategory */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtIncludeBrandCategory { @JsonProperty("primaryadserver") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebid.java index eeece7320bd..9b656fc180c 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebid.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.bidder.Bidder; import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; @@ -12,8 +11,7 @@ *

* Can be used by {@link Bidder}s to unmarshal any request.imp[i].ext. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtPrebid { P prebid; @@ -24,7 +22,7 @@ public class ExtPrebid { * Each bidder should specify their corresponding ExtImp{Bidder} class as a type argument when unmarshaling * extension using this class. *

- * Bidder implementations may safely assume that this extension has been validated by their parameters schema. + * Bidder implementations may safely assume that this extension has been validated by their parameters' schema. */ B bidder; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebidBidders.java b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebidBidders.java index a11559c2a52..6c0a25ebdfe 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebidBidders.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/ExtPrebidBidders.java @@ -1,15 +1,13 @@ package org.prebid.server.proto.openrtb.ext; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.bidder.Bidder; /** * Can be used by {@link Bidder}s to unmarshal any request.ext.prebid.bidders. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtPrebidBidders { /** @@ -17,4 +15,3 @@ public class ExtPrebidBidders { */ JsonNode bidder; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java index dc739edc03b..1b30a856323 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ConsentedProvidersSettings.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ConsentedProvidersSettings { String consentedProviders; 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/ExtAppPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtAppPrebid.java index 398176dbe4e..280cbdd867e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtAppPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtAppPrebid.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; /** @@ -8,8 +7,7 @@ * We are only enforcing that these two properties be strings if they are provided. * They are optional with no current constraints on value. */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtAppPrebid { String source; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java index 3a66ac2374e..49c68685625 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java @@ -1,17 +1,10 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtBidderConfig { - /** - * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd - */ - ExtBidderConfigFpd fpd; - /** * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2 */ diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java deleted file mode 100644 index 27748982011..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class ExtBidderConfigFpd { - - /** - * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.context - */ - JsonNode context; - - /** - * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.user - */ - JsonNode user; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java index 59a6ecc08f7..83203fb60cd 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtBidderConfigOrtb { /** @@ -27,4 +25,9 @@ public class ExtBidderConfigOrtb { * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.user */ ObjectNode user; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.device + */ + ObjectNode device; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java index 642720c3306..ca958582dea 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeal.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].deals[].ext.line */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtDeal { ExtDealLine line; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java index fd8d263832a..ed1a488285f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealLine.java @@ -2,13 +2,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.iab.openrtb.request.Format; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtDealLine { @JsonProperty("lineitemid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealTier.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealTier.java index b661c51c9e7..fd15c95f988 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealTier.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDealTier.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.prebid.bidder.dealTier */ -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtDealTier { String prefix; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceInt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceInt.java index 28fe4c88267..d1204f084b9 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceInt.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDeviceInt.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * ExtDevice defines the contract for bidrequest.device.ext.prebid.interstitial */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtDeviceInt { @JsonProperty("minwidthperc") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevicePrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevicePrebid.java index 4135b662adc..86435bf99ee 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevicePrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDevicePrebid.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; /** * ExtDevice defines the contract for bidrequest.device.ext.prebid */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtDevicePrebid { ExtDeviceInt interstitial; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDooh.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDooh.java index 35131966308..4fc7575efd1 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDooh.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtDooh.java @@ -1,14 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; -import java.util.Objects; - /** * Defines the contract for bidrequest.dooh.ext */ @@ -17,16 +14,8 @@ @ToString(callSuper = true) public class ExtDooh extends FlexibleExtension { - private static final ExtDooh EMPTY = ExtDooh.of(null); - /** * Defines the contract for bidrequest.dooh.ext.data. */ ObjectNode data; - - @JsonIgnore - public boolean isEmpty() { - return Objects.equals(this, EMPTY); - } - } 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..33ac57c13c0 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 @@ -20,6 +20,11 @@ public class ExtImpPrebid { */ ExtStoredRequest storedrequest; + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.profiles + */ + List profiles; + /** * Defines the contract for bidrequest.imp[i].ext.prebid.storedauctionresponse */ @@ -56,4 +61,15 @@ 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; + + /** + * Defines the contract for bidrequest.imp[i].ext.prebid.adunitcode + */ + @JsonProperty("adunitcode") + String adUnitCode; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtMediaTypePriceGranularity.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtMediaTypePriceGranularity.java index 6b5c5d0601e..b931c326116 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtMediaTypePriceGranularity.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtMediaTypePriceGranularity.java @@ -2,15 +2,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Value; /** * Defines the contract for bidrequest.ext.prebid.targeting.mediatypepricegranularity */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtMediaTypePriceGranularity { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtOptions.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtOptions.java index bdde8c4ab90..54a65a3a419 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtOptions.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtOptions.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * ExtRegs defines the contract for ext.prebid.options */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtOptions { @JsonProperty("echovideoattrs") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPriceGranularity.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPriceGranularity.java index ff83f3d86ba..6c148e28c01 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPriceGranularity.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPriceGranularity.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.auction.PriceGranularity; @@ -10,8 +9,7 @@ * Defines the contract for bidrequest.ext.prebid.targeting.pricegranularity and * bidrequest.ext.prebid.targeting.mediatypepricegranularity.banner|video|native */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtPriceGranularity { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPublisherPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPublisherPrebid.java index abbba8f2039..642e27883a2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPublisherPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtPublisherPrebid.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.app|site.publisher.prebid */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtPublisherPrebid { /** 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/ExtRequestCurrency.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestCurrency.java index 320d638fc42..6ece0cf3667 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestCurrency.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestCurrency.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; @@ -9,8 +8,7 @@ /** * Defines the contract for bidrequest.ext.prebid.currency */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestCurrency { /** 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..1380e543991 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 @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; +import org.prebid.server.auction.model.PaaFormat; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.json.deserializer.IntegerFlagDeserializer; @@ -50,6 +51,11 @@ public class ExtRequestPrebid { */ ExtRequestBidAdjustmentFactors bidadjustmentfactors; + /** + * Defines the contract for bidrequest.ext.prebid.bidadjustments + */ + ObjectNode bidadjustments; + /** * Defines the contract for bidrequest.ext.prebid.currency */ @@ -70,6 +76,17 @@ public class ExtRequestPrebid { */ ExtStoredRequest storedrequest; + /** + * Defines the contract for bidrequest.ext.prebid.profiles + */ + List profiles; + + /** + * Defines the contract for bidrequest.ext.prebid.storedauctionresponse + */ + @JsonProperty("storedauctionresponse") + ExtStoredAuctionResponse storedAuctionResponse; + /** * Defines the contract for bidrequest.ext.prebid.cache */ @@ -173,4 +190,14 @@ public class ExtRequestPrebid { */ ExtRequestPrebidSdk sdk; + /** + * Defines the contract for bidrequest.ext.prebid.paaformat + */ + @JsonProperty("paaformat") + PaaFormat paaFormat; + + @JsonProperty("alternatebiddercodes") + ExtRequestPrebidAlternateBidderCodes alternateBidderCodes; + + ObjectNode kvps; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodes.java new file mode 100644 index 00000000000..60dc15956db --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodes.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import lombok.Value; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class ExtRequestPrebidAlternateBidderCodes implements AlternateBidderCodesConfig { + + Boolean enabled; + + Map bidders; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodesBidder.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodesBidder.java new file mode 100644 index 00000000000..61caa45e792 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAlternateBidderCodesBidder.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; +import org.prebid.server.auction.aliases.AlternateBidder; + +import java.util.Set; + +@Value(staticConstructor = "of") +public class ExtRequestPrebidAlternateBidderCodesBidder implements AlternateBidder { + + Boolean enabled; + + @JsonProperty("allowedbiddercodes") + Set allowedBidderCodes; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAmp.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAmp.java index 38ef6b3d25e..7a9c95973cc 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAmp.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidAmp.java @@ -15,4 +15,3 @@ public class ExtRequestPrebidAmp { */ Map data; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidBidderConfig.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidBidderConfig.java index 466197694ff..3bb16185e25 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidBidderConfig.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidBidderConfig.java @@ -1,12 +1,10 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestPrebidBidderConfig { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCache.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCache.java index 026684ae2c1..a3a842ee456 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCache.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCache.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.ext.prebid.cache */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestPrebidCache { public static final ExtRequestPrebidCache EMPTY = new ExtRequestPrebidCache(null, null, null); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheBids.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheBids.java index 2d4a5b2ff2d..2e07762a23d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheBids.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheBids.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestPrebidCacheBids { Integer ttlseconds; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheVastxml.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheVastxml.java index 842ec98429d..cccb422648e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheVastxml.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidCacheVastxml.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestPrebidCacheVastxml { Integer ttlseconds; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java index 990ad7c2ad8..535ff40ae6a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidData.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -10,8 +9,7 @@ /** * Defines the contract for bidrequest.ext.prebid.data */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestPrebidData { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java index 8ad5b5b2ffb..2373c40e886 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidDataEidPermissions.java @@ -1,25 +1,43 @@ package org.prebid.server.proto.openrtb.ext.request; import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Builder; import lombok.Value; import java.util.List; -/** - * Defines the contract for bidrequest.ext.prebid.data.eidPermissions - */ -@Value(staticConstructor = "of") +@Value +@Builder public class ExtRequestPrebidDataEidPermissions { + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.inserter + */ + String inserter; + /** * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.source */ String source; + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.matcher + */ + String matcher; + + /** + * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.mm + */ + Integer mm; + /** * Defines the contract for bidrequest.ext.prebid.data.eidPermissions.bidders */ @JsonFormat(without = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) List bidders; -} + @Deprecated + public static ExtRequestPrebidDataEidPermissions of(String source, List bidders) { + return new ExtRequestPrebidDataEidPermissions(null, source, null, null, bidders); + } +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSchain.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSchain.java index 5eb237bcd72..edfd5b8f918 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSchain.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSchain.java @@ -21,4 +21,3 @@ public class ExtRequestPrebidSchain { */ SupplyChain schain; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtSource.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtSource.java index 4388d90273a..8f205fc85b0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtSource.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtSource.java @@ -13,4 +13,3 @@ public class ExtSource extends FlexibleExtension { SupplyChain schain; } - 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..e692b23cb27 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,19 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iab.openrtb.response.SeatBid; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +import java.util.List; + +@Value(staticConstructor = "of") 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/ExtStoredBidResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredBidResponse.java index a496ae4d4b8..553f5de5481 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredBidResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredBidResponse.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtStoredBidResponse { String bidder; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredRequest.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredRequest.java index 2a9ec41cd3c..af5cacc0ed1 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredRequest.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtStoredRequest.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for ext.prebid.storedrequest */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtStoredRequest { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java index 900a355a9fb..c570e221362 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java @@ -55,8 +55,13 @@ public class ExtUser extends FlexibleExtension { /** * Defines the contract for bidrequest.user.ext.ConsentedProvidersSettings + *

+ * TODO: Remove after PBS 4.0 */ + @Deprecated(forRemoval = true) @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings deprecatedConsentedProvidersSettings; + ConsentedProvidersSettings consentedProvidersSettings; @JsonIgnore diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserPrebid.java index f15b73ab8af..e2eae8f1d52 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserPrebid.java @@ -1,15 +1,13 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Map; /** - * Defines the the contract for bidrequest.user.ext.prebid + * Defines the contract for bidrequest.user.ext.prebid */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtUserPrebid { Map buyeruids; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java index 87ea86f8520..00fff5c4db4 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUserTime.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtUserTime { /** diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java index 732ddca6236..d619ed27e80 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java @@ -9,6 +9,8 @@ public enum ImpMediaType { @JsonProperty("native") xNative, video, + @JsonProperty("video-instream") + video_instream, @JsonProperty("video-outstream") video_outstream; @@ -16,6 +18,7 @@ public enum ImpMediaType { public String toString() { return this == xNative ? "native" : this == video_outstream ? "video-outstream" + : this == video_instream ? "video-instream" : super.toString(); } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/aceex/ExtImpAceex.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aceex/ExtImpAceex.java index fb26ec0a898..aebf8beb180 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/aceex/ExtImpAceex.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aceex/ExtImpAceex.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.aceex; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpAceex { @JsonProperty("accountid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java index 76c6c369114..3056042416d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/acuity/ExtImpAcuityads.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.acuity; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAcuityads { String host; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java new file mode 100644 index 00000000000..fb8cc3ed7b6 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adagio/ExtImpAdagio.java @@ -0,0 +1,19 @@ +package org.prebid.server.proto.openrtb.ext.request.adagio; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdagio { + + @JsonProperty("organizationId") + String organizationId; + + String placement; + + String site; + + String pagetype; + + String category; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adelement/ExtImpAdelement.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adelement/ExtImpAdelement.java index 827e7892c54..d17b5882672 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adelement/ExtImpAdelement.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adelement/ExtImpAdelement.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.adelement; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpAdelement { @JsonProperty("supply_id") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adgeneration/ExtImpAdgeneration.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adgeneration/ExtImpAdgeneration.java index 8646b462dfd..5bc6e76dd6f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adgeneration/ExtImpAdgeneration.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adgeneration/ExtImpAdgeneration.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.adgeneration; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdgeneration { String id; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java index e21e859bc67..1375a984aee 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adhese/ExtImpAdhese.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.adhese; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.adhese */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdhese { String account; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adkernel/ExtImpAdkernel.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adkernel/ExtImpAdkernel.java index c3f990c8195..442db44032a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adkernel/ExtImpAdkernel.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adkernel/ExtImpAdkernel.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.adkernel; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdkernel { @JsonProperty("zoneId") 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/adnuntius/ExtImpAdnuntius.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntius.java index c54dab4b520..e110b122bb0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntius.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntius.java @@ -21,4 +21,6 @@ public class ExtImpAdnuntius { @JsonProperty("bidType") String bidType; + + ExtImpAdnuntiusTargeting targeting; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntiusTargeting.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntiusTargeting.java new file mode 100644 index 00000000000..508fcb0dfba --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adnuntius/ExtImpAdnuntiusTargeting.java @@ -0,0 +1,28 @@ +package org.prebid.server.proto.openrtb.ext.request.adnuntius; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Map; + +@Value +@Builder +public class ExtImpAdnuntiusTargeting { + + @JsonProperty("c") + List category; + + List segments; + + List keywords; + + @JsonProperty("kv") + Map> keyValues; + + @JsonProperty("auml") + List adUnitMatchingLabel; + + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adocean/ExtImpAdocean.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adocean/ExtImpAdocean.java index 4a1eb21f565..aa364d222ae 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adocean/ExtImpAdocean.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adocean/ExtImpAdocean.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.adocean; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.adocean */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdocean { @JsonProperty("emitterPrefix") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java deleted file mode 100644 index b839645945b..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adoppler/ExtImpAdoppler.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.adoppler; - -import lombok.AllArgsConstructor; -import lombok.Value; - -@AllArgsConstructor(staticName = "of") -@Value -public class ExtImpAdoppler { - - String adunit; - String client; -} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java index 6e37becb9d9..f0696753d20 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adot/ExtImpAdot.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.adot; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.adot */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdot { Boolean parallax; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adpone/ExtImpAdpone.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adpone/ExtImpAdpone.java index c109a798080..f8086455825 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adpone/ExtImpAdpone.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adpone/ExtImpAdpone.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.adpone; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; @Value -@AllArgsConstructor public class ExtImpAdpone { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java index 8a5c2947648..91582df3529 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adprime/ExtImpAdprime.java @@ -1,7 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request.adprime; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -9,8 +8,7 @@ /** * Defines the contract for bidRequest.imp[i].ext.adprime */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdprime { @JsonProperty("TagID") 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/aduptech/ExtImpAduptech.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java new file mode 100644 index 00000000000..6e92ad45db7 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/aduptech/ExtImpAduptech.java @@ -0,0 +1,21 @@ +package org.prebid.server.proto.openrtb.ext.request.aduptech; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAduptech { + + String publisher; + + String placement; + + String query; + + Boolean adtest; + + Boolean debug; + + ObjectNode ext; +} + diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/advangelists/ExtImpAdvangelists.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/advangelists/ExtImpAdvangelists.java index 8591c2703c6..352762c8e08 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/advangelists/ExtImpAdvangelists.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/advangelists/ExtImpAdvangelists.java @@ -1,17 +1,14 @@ package org.prebid.server.proto.openrtb.ext.request.advangelists; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.advangelists */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdvangelists { String pubid; String placement; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adverxo/ExtImpAdverxo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adverxo/ExtImpAdverxo.java new file mode 100644 index 00000000000..86a90653464 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adverxo/ExtImpAdverxo.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.adverxo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAdverxo { + + @JsonProperty("adUnitId") + Integer adUnitId; + + String auth; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adview/ExtImpAdview.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adview/ExtImpAdview.java index 886d3e5f18f..a1239b3aea0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adview/ExtImpAdview.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adview/ExtImpAdview.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.adview; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpAdview { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java index 79542d3184e..4d205b12229 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adxcg/ExtImpAdxcg.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.adxcg; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.adxcg */ -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpAdxcg { String adzoneid; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java index 19f4172942a..4d64b1f191d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/adyoulike/ExtImpAdyoulike.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.adyoulike; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.adyoulike */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAdyoulike { String placement; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/afront/ExtImpAfront.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/afront/ExtImpAfront.java new file mode 100644 index 00000000000..d4502835ed8 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/afront/ExtImpAfront.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.afront; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAfront { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("sourceId") + String sourceId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java new file mode 100644 index 00000000000..4e57d8d6a12 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/akcelo/ExtImpAkcelo.java @@ -0,0 +1,16 @@ +package org.prebid.server.proto.openrtb.ext.request.akcelo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpAkcelo { + + @JsonProperty("adUnitId") + Integer adUnitId; + + @JsonProperty("siteId") + String siteId; + + Integer test; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java index 082bd748712..f666b99f289 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/algorix/ExtImpAlgorix.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.algorix; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Algorix Ext Imp */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAlgorix { String sid; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java index 1ad50749816..f3206a5e01c 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/amx/ExtImpAmx.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.amx; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpAmx { @JsonProperty("tagId") 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/axis/ExtImpAxis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axis/ExtImpAxis.java index 7791f7210c1..8750a0b2eb8 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/axis/ExtImpAxis.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axis/ExtImpAxis.java @@ -9,4 +9,3 @@ public class ExtImpAxis { String token; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java index 7ec0fbaf095..104fb2a6085 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/axonix/ExtImpAxonix.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.axonix; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpAxonix { @JsonProperty("supplyId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfront.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfront.java index d9c924a824e..1d1b9e369aa 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfront.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfront.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.beachfront; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpBeachfront { @JsonProperty("appId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfrontAppIds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfrontAppIds.java index 6ec6d7e2f5a..fe4e4887b5a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfrontAppIds.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beachfront/ExtImpBeachfrontAppIds.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.beachfront; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpBeachfrontAppIds { String video; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beintoo/ExtImpBeintoo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beintoo/ExtImpBeintoo.java index 6480370c041..0aebfc01482 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/beintoo/ExtImpBeintoo.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/beintoo/ExtImpBeintoo.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.beintoo; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpBeintoo { @JsonProperty("tagid") 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/bidmyadz/ExtImpBidmyadz.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java index 8f6bf79a71d..bd87438c8f3 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidmyadz/ExtImpBidmyadz.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.bidmyadz; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpBidmyadz { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java index 8cf58c3865c..875e26f145f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidscube/ExtImpBidscube.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.bidscube; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpBidscube { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java new file mode 100644 index 00000000000..3eb1696f672 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bidtheatre/ExtImpBidTheatre.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.bidtheatre; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBidTheatre { + + @JsonProperty("publisherId") + String publisherId; + +} 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/blis/ExtImpBlis.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blis/ExtImpBlis.java new file mode 100644 index 00000000000..0b0807ecf3e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/blis/ExtImpBlis.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.blis; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBlis { + + @JsonProperty("spid") + String supplyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java index eb6866b287f..12cc86cf1ca 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/bmtm/ExtImpBmtm.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.bmtm; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpBmtm { String placementId; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/boldwinrapid/ExtImpBoldwinRapid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/boldwinrapid/ExtImpBoldwinRapid.java new file mode 100644 index 00000000000..a546a4bd8bb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/boldwinrapid/ExtImpBoldwinRapid.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.boldwinrapid; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpBoldwinRapid { + + String pid; + + String tid; +} 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/ccx/ExtImpCcx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ccx/ExtImpCcx.java deleted file mode 100644 index 61a756c2932..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ccx/ExtImpCcx.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request.ccx; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Value; - -@Value(staticConstructor = "of") -public class ExtImpCcx { - - @JsonProperty("placementId") - Integer placementId; -} 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..b543c6b44e5 --- /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 +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/connatix/ExtImpConnatix.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connatix/ExtImpConnatix.java new file mode 100644 index 00000000000..03bcfd35572 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/connatix/ExtImpConnatix.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.connatix; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class ExtImpConnatix { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("viewabilityPercentage") + BigDecimal viewabilityPercentage; + +} 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..a75c4846f64 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 @@ -1,20 +1,19 @@ package org.prebid.server.proto.openrtb.ext.request.connectad; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") 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..696aefcde28 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 @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.consumable; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.consumable */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpConsumable { @JsonProperty("networkId") @@ -22,4 +20,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/contxtful/ExtImpContxtful.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/contxtful/ExtImpContxtful.java new file mode 100644 index 00000000000..14e38b06e9c --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/contxtful/ExtImpContxtful.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.contxtful; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpContxtful { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("customerId") + String customerId; +} 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..858deb6f05f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/copper6ssp/ImpExtCopper6Ssp.java @@ -0,0 +1,14 @@ +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/cpmstar/ExtImpCpmStar.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/cpmstar/ExtImpCpmStar.java index 0aa8d990c2e..1954ac05a03 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/cpmstar/ExtImpCpmStar.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/cpmstar/ExtImpCpmStar.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.cpmstar; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpCpmStar { @JsonProperty("placementId") @@ -14,4 +12,3 @@ public class ExtImpCpmStar { @JsonProperty("subpoolId") Integer subPoolId; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/datablocks/ExtImpDatablocks.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/datablocks/ExtImpDatablocks.java index 9e33a4f7d81..19eedd5b3a1 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/datablocks/ExtImpDatablocks.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/datablocks/ExtImpDatablocks.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.datablocks; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.datablocks */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpDatablocks { @JsonProperty("sourceId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java index d009f9b014e..00152f7ac18 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/decenterads/ExtImpDecenterads.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.decenterads; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.decenterads */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpDecenterads { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java index 235c5e6c7f5..618f7dd2bef 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/deepintent/ExtImpDeepintent.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.deepintent; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpDeepintent { @JsonProperty("tagId") 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/elementaltv/ExtImpElementalTV.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/elementaltv/ExtImpElementalTV.java new file mode 100644 index 00000000000..bdd594996b4 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/elementaltv/ExtImpElementalTV.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.elementaltv; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpElementalTV { + + String adunit; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/emxdigital/ExtImpEmxDigital.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/emxdigital/ExtImpEmxDigital.java index d93597928b4..d96d0650b2b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/emxdigital/ExtImpEmxDigital.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/emxdigital/ExtImpEmxDigital.java @@ -1,17 +1,14 @@ package org.prebid.server.proto.openrtb.ext.request.emxdigital; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.emx_digital */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpEmxDigital { String tagid; String bidfloor; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/eplanning/ExtImpEplanning.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/eplanning/ExtImpEplanning.java index 6e012ef909b..2fb09d0f9dc 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/eplanning/ExtImpEplanning.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/eplanning/ExtImpEplanning.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.eplanning; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.eplanning */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpEplanning { @JsonProperty("ci") 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/exco/ExtImpExco.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/exco/ExtImpExco.java new file mode 100644 index 00000000000..91bef799cbd --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/exco/ExtImpExco.java @@ -0,0 +1,17 @@ +package org.prebid.server.proto.openrtb.ext.request.exco; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpExco { + + @JsonProperty("accountId") + String accountId; + + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("tagId") + String tagId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java new file mode 100644 index 00000000000..304cd887ebb --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/flatads/ExtImpFlatads.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.flatads; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpFlatads { + + String token; + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java index eb297969465..f5d0276f429 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/freewheelssp/ExtImpFreewheelSSP.java @@ -8,4 +8,10 @@ public class ExtImpFreewheelSSP { @JsonProperty("zoneId") String zoneId; + + String customSiteSectionId; + + String networkId; + + String profileId; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamma/ExtImpGamma.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamma/ExtImpGamma.java index 7f42cb2d828..cce179a8f0a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamma/ExtImpGamma.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamma/ExtImpGamma.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.gamma; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.gamma */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpGamma { String id; @@ -16,4 +14,3 @@ public class ExtImpGamma { String wid; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamoshi/ExtImpGamoshi.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamoshi/ExtImpGamoshi.java index a03fb7d6298..e0b961953b5 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamoshi/ExtImpGamoshi.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gamoshi/ExtImpGamoshi.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.gamoshi; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpGamoshi { @JsonProperty("supplyPartnerId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java index 6f0803c9a6e..21f5cffd118 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgum.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.gumgum; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigInteger; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpGumgum { String zone; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumBanner.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumBanner.java index 857125e2847..a4eacbd9046 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumBanner.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumBanner.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.gumgum; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpGumgumBanner { Long slot; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java index a0880109568..06bcfb14b4e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/gumgum/ExtImpGumgumVideo.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.gumgum; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpGumgumVideo { @JsonProperty("irisid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtImpImds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtImpImds.java index 1cf9158cd79..8d6d36297b4 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtImpImds.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtImpImds.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.imds; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.imds */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpImds { @JsonProperty("seatId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtRequestImds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtRequestImds.java index 55d7875e7e2..86df035084d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtRequestImds.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/imds/ExtRequestImds.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.imds; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.ext */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestImds { @JsonProperty("seatId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/improvedigital/ExtImpImprovedigital.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/improvedigital/ExtImpImprovedigital.java index 150f947ee60..ff1a4e04160 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/improvedigital/ExtImpImprovedigital.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/improvedigital/ExtImpImprovedigital.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.improvedigital; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpImprovedigital { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/inmobi/ExtImpInmobi.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/inmobi/ExtImpInmobi.java index 8bc8b52c619..e2024710797 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/inmobi/ExtImpInmobi.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/inmobi/ExtImpInmobi.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.inmobi; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpInmobi { String plc; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java new file mode 100644 index 00000000000..067680eb0df --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.insticator; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpInsticator { + + @JsonProperty("adUnitId") + String adUnitId; + + @JsonProperty("publisherId") + String publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java index fc3427f745e..4e41693876c 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/interactiveoffers/ExtImpInteractiveoffers.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.interactiveoffers; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.interactiveoffers */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpInteractiveoffers { @JsonProperty("partnerId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java index 1de204b5755..88a0267e71a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/ExtImpInvibes.java @@ -1,12 +1,10 @@ package org.prebid.server.proto.openrtb.ext.request.invibes; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.proto.openrtb.ext.request.invibes.model.InvibesDebug; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpInvibes { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java index f3df563e03f..33df812b551 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/invibes/model/InvibesDebug.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.invibes.model; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class InvibesDebug { @JsonProperty("testBvid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java index 12508ccbb1c..09d49e9406a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ix/ExtImpIx.java @@ -2,13 +2,11 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpIx { @JsonProperty("siteId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java index ee9d29f28e7..e5526aefd8a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/jixie/ExtImpJixie.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.jixie; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpJixie { String unit; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java index 05cc2efe58f..6c203dc008d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kayzen/ExtImpKayzen.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.kayzen; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpKayzen { String zone; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kidoz/ExtImpKidoz.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kidoz/ExtImpKidoz.java index 8bc93217482..03b47a9bcfe 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kidoz/ExtImpKidoz.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kidoz/ExtImpKidoz.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.kidoz; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpKidoz { String accessToken; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kobler/ExtImpKobler.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kobler/ExtImpKobler.java new file mode 100644 index 00000000000..ba8e94bb6a9 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kobler/ExtImpKobler.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.kobler; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpKobler { + + Boolean test; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java new file mode 100644 index 00000000000..5abaf1be0f5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/kueezrtb/KueezRtbImpExt.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.kueezrtb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class KueezRtbImpExt { + + @JsonProperty("cId") + String connectionId; + +} 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/lockerdome/ExtImpLockerdome.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/lockerdome/ExtImpLockerdome.java index b9598677939..24c010c1ae5 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/lockerdome/ExtImpLockerdome.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/lockerdome/ExtImpLockerdome.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.lockerdome; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.lockerdome */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpLockerdome { @JsonProperty("adUnitId") 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/lunamedia/ExtImpLunamedia.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/lunamedia/ExtImpLunamedia.java index 5e6938c57a1..21b71ff98e6 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/lunamedia/ExtImpLunamedia.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/lunamedia/ExtImpLunamedia.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.lunamedia; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.lunamedia */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpLunamedia { String pubid; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java new file mode 100644 index 00000000000..6725007f682 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madsense/ExtImpMadsense.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.madsense; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMadsense { + + String companyId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java index 8ca5fc64015..946378c9350 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/madvertise/ExtImpMadvertise.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.madvertise; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpMadvertise { @JsonProperty("zoneId") 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/mediasquare/ExtImpMediasquare.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediasquare/ExtImpMediasquare.java new file mode 100644 index 00000000000..1c71f176881 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mediasquare/ExtImpMediasquare.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.mediasquare; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMediasquare { + + String owner; + + String code; +} 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..0c3a8fd08ab --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/missena/ExtImpMissena.java @@ -0,0 +1,25 @@ +package org.prebid.server.proto.openrtb.ext.request.missena; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder(toBuilder = true) +public class ExtImpMissena { + + @JsonProperty("apiKey") + String apiKey; + + List formats; + + String placement; + + @JsonProperty("test") + String testMode; + + ObjectNode settings; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java index b19f6fa5fc8..0e1d423712f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobfoxpb/ExtImpMobfoxpb.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.mobfoxpb; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.mobfoxpb */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpMobfoxpb { @JsonProperty("TagID") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java index 8fd14c9a887..8e2b76d04a8 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobilefuse/ExtImpMobilefuse.java @@ -1,22 +1,14 @@ package org.prebid.server.proto.openrtb.ext.request.mobilefuse; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.mobilefuse */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpMobilefuse { @JsonProperty("placement_id") Integer placementId; - - @JsonProperty("pub_id") - Integer publisherId; - - @JsonProperty("tagid_src") - String tagidSrc; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java new file mode 100644 index 00000000000..ac8009a76d2 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/mobkoi/ExtImpMobkoi.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.mobkoi; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpMobkoi { + + @JsonProperty("placementId") + String placementId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/BidExtNativery.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/BidExtNativery.java new file mode 100644 index 00000000000..3c7ee391327 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/BidExtNativery.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.nativery; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class BidExtNativery { + + String bidAdMediaType; + + List bidAdvDomains; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/ExtImpNativery.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/ExtImpNativery.java new file mode 100644 index 00000000000..cd1554111e0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nativery/ExtImpNativery.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.nativery; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpNativery { + + @JsonProperty("widgetId") + String widgetId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nextmillennium/ExtImpNextMillennium.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nextmillennium/ExtImpNextMillennium.java index ed67279ea9a..d262fe0bfbe 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nextmillennium/ExtImpNextMillennium.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nextmillennium/ExtImpNextMillennium.java @@ -1,11 +1,20 @@ package org.prebid.server.proto.openrtb.ext.request.nextmillennium; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; +import java.util.List; + @Value(staticConstructor = "of") public class ExtImpNextMillennium { String placementId; String groupId; + + @JsonProperty("adSlots") + List adSlots; + + @JsonProperty("allowedAds") + List allowedAds; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java new file mode 100644 index 00000000000..5a44dfd868f --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/nexx360/ExtImpNexx360.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.nexx360; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpNexx360 { + + @JsonProperty("tagId") + String tagId; + + String placement; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java new file mode 100644 index 00000000000..f6a96821d44 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.omx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOms { + + String pid; + + @JsonProperty("publisherId") + Integer publisherId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java index 18cb4dd8ae2..92b592a46a3 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/onetag/ExtImpOnetag.java @@ -2,11 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpOnetag { @JsonProperty("pubId") 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..0b528f92984 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 @@ -1,24 +1,15 @@ package org.prebid.server.proto.openrtb.ext.request.openweb; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -import java.math.BigDecimal; - -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "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/operaads/ExtImpOperaads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/operaads/ExtImpOperaads.java index 4380a5eda8a..c713ae17ae7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/operaads/ExtImpOperaads.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/operaads/ExtImpOperaads.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.operaads; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpOperaads { @JsonProperty("placementId") 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/orbidder/ExtImpOrbidder.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/orbidder/ExtImpOrbidder.java index a22df1038c0..a25e67b28b9 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/orbidder/ExtImpOrbidder.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/orbidder/ExtImpOrbidder.java @@ -1,12 +1,10 @@ package org.prebid.server.proto.openrtb.ext.request.orbidder; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpOrbidder { String accountId; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java index a7d771bd1be..04f00bb4f3a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrain.java @@ -1,12 +1,10 @@ package org.prebid.server.proto.openrtb.ext.request.outbrains; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpOutbrain { ExtImpOutbrainPublisher publisher; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java index ba613cc8ff4..05261f9fb38 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/outbrains/ExtImpOutbrainPublisher.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.outbrains; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpOutbrainPublisher { String id; 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/pangle/ExtImpPangle.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java index 8746d804644..2650654f848 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pangle/ExtImpPangle.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.pangle; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpPangle { String token; 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/pubmatic/ExtImpPubmaticKeyVal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmaticKeyVal.java index 56bf9b9b488..c04587cd433 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmaticKeyVal.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubmatic/ExtImpPubmaticKeyVal.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request.pubmatic; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -8,8 +7,7 @@ /** * Defines the contract for bidrequest.imp[i].ext.pubmatic.keywords[i] */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpPubmaticKeyVal { String key; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubnative/ExtImpPubnative.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubnative/ExtImpPubnative.java index fe99f0c207b..9fa8bab85fa 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubnative/ExtImpPubnative.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pubnative/ExtImpPubnative.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.pubnative; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.pubnative */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpPubnative { Integer zoneId; 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/pulsepoint/ExtImpPulsepoint.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java index 85625367548..7691faefbc6 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/pulsepoint/ExtImpPulsepoint.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.pulsepoint; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpPulsepoint { @JsonProperty("cp") 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/rediads/ExtImpRediads.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rediads/ExtImpRediads.java new file mode 100644 index 00000000000..e876780fac8 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/rediads/ExtImpRediads.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.rediads; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpRediads { + + String accountId; + + String slot; + + String endpoint; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/relevantdigital/ExtImpRelevantDigital.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/relevantdigital/ExtImpRelevantDigital.java index ce4840ab050..b62462aea4d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/relevantdigital/ExtImpRelevantDigital.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/relevantdigital/ExtImpRelevantDigital.java @@ -4,7 +4,7 @@ import lombok.Builder; import lombok.Value; -@Value(staticConstructor = "of") +@Value @Builder public class ExtImpRelevantDigital { diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/resetdigital/ExtImpResetDigital.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/resetdigital/ExtImpResetDigital.java new file mode 100644 index 00000000000..78cf004662e --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/resetdigital/ExtImpResetDigital.java @@ -0,0 +1,9 @@ +package org.prebid.server.proto.openrtb.ext.request.resetdigital; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpResetDigital { + + String placementId; +} 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/seedtag/ExtImpSeedtag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java new file mode 100644 index 00000000000..364b63fb7dc --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java @@ -0,0 +1,12 @@ +package org.prebid.server.proto.openrtb.ext.request.seedtag; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSeedtag { + + @JsonProperty("adUnitId") + String adUnitId; + +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java index 5a6330711fe..abab2ee4e84 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sharethrough/ExtImpSharethrough.java @@ -1,6 +1,5 @@ package org.prebid.server.proto.openrtb.ext.request.sharethrough; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -8,8 +7,7 @@ /** * Defines the contract for bidRequest.imp[i].ext.sharethrough */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpSharethrough { String pkey; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/showheroes/ExtImpShowheroes.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/showheroes/ExtImpShowheroes.java new file mode 100644 index 00000000000..77b65257fb0 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/showheroes/ExtImpShowheroes.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.showheroes; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpShowheroes { + + @JsonProperty("unitId") + String unitId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java index 9f6ef557196..b60ef96a563 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smaato/ExtImpSmaato.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.smaato; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpSmaato { @JsonProperty("publisherId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java index 674ef8733b3..efc8de91a5f 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartadserver/ExtImpSmartadserver.java @@ -1,14 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.smartadserver; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -/** - * Defines the contract for bidrequest.imp[i].ext.smartadserver - */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpSmartadserver { @JsonProperty("siteId") @@ -22,4 +17,7 @@ public class ExtImpSmartadserver { @JsonProperty("networkId") Integer networkId; + + @JsonProperty(value = "programmaticGuaranteed", access = JsonProperty.Access.WRITE_ONLY) + boolean programmaticGuaranteed; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smarthub/ExtImpSmarthub.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smarthub/ExtImpSmarthub.java index 068c64ca4bb..058688502a8 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smarthub/ExtImpSmarthub.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smarthub/ExtImpSmarthub.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.smarthub; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpSmarthub { @JsonProperty("partnerName") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtImpSmartrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtImpSmartrtb.java index 27b5bb48f00..4f1cf5f15b2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtImpSmartrtb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtImpSmartrtb.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.smartrtb; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpSmartrtb { String pubId; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtRequestSmartrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtRequestSmartrtb.java index 5452f1bef92..b38895aad9a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtRequestSmartrtb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartrtb/ExtRequestSmartrtb.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.smartrtb; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtRequestSmartrtb { String pubId; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java index 2fa7b02cd85..d49fff0be81 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smartyads/ExtImpSmartyAds.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.smartyads; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpSmartyAds { @JsonProperty("accountid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java new file mode 100644 index 00000000000..ec08ec3a957 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smilewanted/ExtImpSmilewanted.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.smilewanted; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmilewanted { + + @JsonProperty("zoneId") + String zoneId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java new file mode 100644 index 00000000000..6f7e8775a91 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/smoot/ExtImpSmoot.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.smoot; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSmoot { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} 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/sovrn/ExtImpSovrn.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrn/ExtImpSovrn.java index f9370eb9e74..8177d46ae8a 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrn/ExtImpSovrn.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrn/ExtImpSovrn.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.request.sovrn; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class ExtImpSovrn { String tagid; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrnxsp/ExtImpSovrnXsp.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrnxsp/ExtImpSovrnXsp.java index 909b0ec179a..cacaf90fb0d 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrnxsp/ExtImpSovrnXsp.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sovrnxsp/ExtImpSovrnXsp.java @@ -5,7 +5,7 @@ import lombok.Value; @Builder -@Value(staticConstructor = "of") +@Value public class ExtImpSovrnXsp { @JsonProperty("pub_id") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java new file mode 100644 index 00000000000..cb9bda90558 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/sparteo/ExtImpSparteo.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.request.sparteo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpSparteo { + + @JsonProperty("networkId") + String networkId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/stroeercore/ExtImpStroeerCore.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/stroeercore/ExtImpStroeerCore.java index b4f89021765..00eeabf53db 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/stroeercore/ExtImpStroeerCore.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/stroeercore/ExtImpStroeerCore.java @@ -9,4 +9,3 @@ public class ExtImpStroeerCore { @JsonProperty("sid") String slotId; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java index cf4ee8b2834..98c0c8de77b 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/tappx/ExtImpTappx.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.tappx; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpTappx { String host; @@ -27,4 +25,3 @@ public class ExtImpTappx { List bcrid; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpOutTelaria.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpOutTelaria.java index c58ff43f2ea..34fe9a82d26 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpOutTelaria.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpOutTelaria.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.telaria; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpOutTelaria { @JsonProperty("originalTagid") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpTelaria.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpTelaria.java index 537ddf825de..c4b21399849 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpTelaria.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/telaria/ExtImpTelaria.java @@ -2,11 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpTelaria { @JsonProperty("adCode") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/teqblaze/ExtImpTeqblaze.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/teqblaze/ExtImpTeqblaze.java new file mode 100644 index 00000000000..4ffb9f34f08 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/teqblaze/ExtImpTeqblaze.java @@ -0,0 +1,14 @@ +package org.prebid.server.proto.openrtb.ext.request.teqblaze; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpTeqblaze { + + @JsonProperty("placementId") + String placementId; + + @JsonProperty("endpointId") + String endpointId; +} 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..4012942097b --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/theadx/ExtImpTheadx.java @@ -0,0 +1,21 @@ +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..91a3b8e14b2 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thetradedesk/ExtImpTheTradeDesk.java @@ -0,0 +1,14 @@ +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; + + @JsonProperty("supplySourceId") + String supplySourceId; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thirtythreeacross/ExtImpThirtyThreeAcross.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thirtythreeacross/ExtImpThirtyThreeAcross.java index e18c9fb1583..a22e4df5f79 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/thirtythreeacross/ExtImpThirtyThreeAcross.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/thirtythreeacross/ExtImpThirtyThreeAcross.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.thirtythreeacross; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.33across */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpThirtyThreeAcross { @JsonProperty("siteId") 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/triplelift/ExtImpTriplelift.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/triplelift/ExtImpTriplelift.java index 473c045e2ba..dae544589bf 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/triplelift/ExtImpTriplelift.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/triplelift/ExtImpTriplelift.java @@ -1,7 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request.triplelift; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; import java.math.BigDecimal; @@ -9,8 +8,7 @@ /** * Defines the contract for bidRequest.imp[i].ext.triplelift */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpTriplelift { @JsonProperty("inventoryCode") @@ -18,4 +16,3 @@ public class ExtImpTriplelift { BigDecimal floor; } - diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ucfunnel/ExtImpUcfunnel.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ucfunnel/ExtImpUcfunnel.java index 68eb4336841..4c9b4f65040 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ucfunnel/ExtImpUcfunnel.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ucfunnel/ExtImpUcfunnel.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.ucfunnel; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpUcfunnel { String adunitid; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java index ce32cdb7050..83e0c1c2783 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/unicorn/ExtImpUnicorn.java @@ -1,11 +1,9 @@ package org.prebid.server.proto.openrtb.ext.request.unicorn; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Value; -@AllArgsConstructor(staticName = "of") @Value @Builder(toBuilder = true) public class ExtImpUnicorn { 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/vrtcal/ExtImpVrtcal.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vrtcal/ExtImpVrtcal.java index 899a67be538..182a5eb8487 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/vrtcal/ExtImpVrtcal.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/vrtcal/ExtImpVrtcal.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.vrtcal; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.Vrtcal */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpVrtcal { @JsonProperty("Just_an_unused_vrtcal_param") 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/yahooads/ExtImpYahooAds.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yahooads/ExtImpYahooAds.java index 23846e51bd2..e7bfd790fa9 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yahooads/ExtImpYahooAds.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yahooads/ExtImpYahooAds.java @@ -1,10 +1,8 @@ package org.prebid.server.proto.openrtb.ext.request.yahooads; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpYahooAds { String dcn; 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/yieldmo/ExtImpYieldmo.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldmo/ExtImpYieldmo.java index 5f9bd42f9ae..227750be1e2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldmo/ExtImpYieldmo.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldmo/ExtImpYieldmo.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.yieldmo; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.yieldmo */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpYieldmo { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldone/ExtImpYieldone.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldone/ExtImpYieldone.java index 48ace41d819..12b59bfd92e 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldone/ExtImpYieldone.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/yieldone/ExtImpYieldone.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.yieldone; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidrequest.imp[i].ext.yieldone */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpYieldone { @JsonProperty("placementId") diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeroclickfraud/ExtImpZeroclickfraud.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeroclickfraud/ExtImpZeroclickfraud.java index baeffdcda8e..d74b6d512dd 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeroclickfraud/ExtImpZeroclickfraud.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/zeroclickfraud/ExtImpZeroclickfraud.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.request.zeroclickfraud; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidRequest.imp[i].ext.zeroclickfraud */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtImpZeroclickfraud { @JsonProperty("sourceId") 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/CacheAsset.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/CacheAsset.java index 6a490afc7fb..99748f0f5c0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/CacheAsset.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/CacheAsset.java @@ -1,7 +1,6 @@ package org.prebid.server.proto.openrtb.ext.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** @@ -10,8 +9,7 @@ * and * bidresponse.seatbid.bid[i].ext.prebid.cache.vastXml */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CacheAsset { String url; 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/Events.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/Events.java index f9d60fe227a..11c0594fbbc 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/Events.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/Events.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.response; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidresponse.seatbid.bid[i].ext.prebid.events */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class Events { String win; diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAdPod.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAdPod.java index 655ed5ddbe4..530e5321fa4 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAdPod.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtAdPod.java @@ -1,12 +1,10 @@ package org.prebid.server.proto.openrtb.ext.response; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtAdPod { Integer podid; 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/ExtBidPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java index d116447caed..afc46770799 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java @@ -40,4 +40,6 @@ public class ExtBidPrebid { @JsonProperty("passthrough") JsonNode passThrough; + + Integer rank; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java index b7295616a82..eaba36abca0 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java @@ -67,4 +67,6 @@ public class ExtBidPrebidMeta { @JsonProperty("secondaryCatIds") List secondaryCategoryIdList; + String seat; + } 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/ExtBidResponse.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java index 115ce7b3ff9..6f4b3904fd7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidResponse.java @@ -48,6 +48,11 @@ public class ExtBidResponse { */ Map usersync; + /** + * Defines the contract for bidresponse.ext.igi + */ + List igi; + /** * Defines the contract for bidresponse.ext.prebid */ 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..6ab54f9f9ae 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,30 +1,14 @@ 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 */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") 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/ExtIgi.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgi.java new file mode 100644 index 00000000000..a68ed9cdf6a --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgi.java @@ -0,0 +1,20 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +/** + * Defines the contract for bidresponse.ext.igi + */ +@Builder(toBuilder = true) +@Value +public class ExtIgi { + + String impid; + + List igb; + + List igs; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgb.java new file mode 100644 index 00000000000..aca3758c7aa --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgb.java @@ -0,0 +1,22 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class ExtIgiIgb { + + String origin; + + Double maxbid; + + @Builder.Default + String cur = "USD"; + + JsonNode pbs; + + ObjectNode ps; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgs.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgs.java new file mode 100644 index 00000000000..305d211a4b5 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgs.java @@ -0,0 +1,18 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class ExtIgiIgs { + + @JsonProperty("impid") + String impId; + + ObjectNode config; + + ExtIgiIgsExt ext; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgsExt.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgsExt.java new file mode 100644 index 00000000000..979b7fe6e13 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtIgiIgsExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.proto.openrtb.ext.response; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtIgiIgsExt { + + String bidder; + + String adapter; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseCache.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseCache.java index cc63a9726e5..f13206d51c7 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseCache.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseCache.java @@ -1,14 +1,12 @@ package org.prebid.server.proto.openrtb.ext.response; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidresponse.seatbid.bid[i].ext.prebid.cache */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtResponseCache { CacheAsset bids; 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..fd2328afc18 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 @@ -1,7 +1,6 @@ package org.prebid.server.proto.openrtb.ext.response; import com.iab.openrtb.request.BidRequest; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; @@ -10,8 +9,7 @@ /** * Defines the contract for bidresponse.ext.debug */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtResponseDebug { /** @@ -24,11 +22,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/ExtResponseVideoTargeting.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseVideoTargeting.java index 589b53c82f2..fdd2a33ac08 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseVideoTargeting.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtResponseVideoTargeting.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.openrtb.ext.response; -import lombok.AllArgsConstructor; import lombok.Value; /** * Defines the contract for bidresponse.ext.debug */ -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class ExtResponseVideoTargeting { String hbPb; @@ -16,4 +14,3 @@ public class ExtResponseVideoTargeting { String hbCacheID; } - 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/proto/request/CookieSyncRequest.java b/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java index b45221478fe..97e589134fb 100644 --- a/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java +++ b/src/main/java/org/prebid/server/proto/request/CookieSyncRequest.java @@ -59,4 +59,3 @@ public enum FilterType { include, exclude } } - diff --git a/src/main/java/org/prebid/server/proto/request/Targeting.java b/src/main/java/org/prebid/server/proto/request/Targeting.java index 246f33d45ed..804e7e9b179 100644 --- a/src/main/java/org/prebid/server/proto/request/Targeting.java +++ b/src/main/java/org/prebid/server/proto/request/Targeting.java @@ -1,34 +1,25 @@ package org.prebid.server.proto.request; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@Value -@AllArgsConstructor(staticName = "of") +@Value(staticConstructor = "of") public class Targeting { - private static final Targeting EMPTY = Targeting.of(null, null, null); - /* * Will be mapped to ext.prebid.data */ - List bidders; + /* * Will be mapped to site.ext.data */ - ObjectNode site; + /* * Will be mapped to user.ext.data */ - ObjectNode user; - - public static Targeting empty() { - return EMPTY; - } } diff --git a/src/main/java/org/prebid/server/proto/response/CookieSyncResponse.java b/src/main/java/org/prebid/server/proto/response/CookieSyncResponse.java index b7dae32ce6e..9b590390b83 100644 --- a/src/main/java/org/prebid/server/proto/response/CookieSyncResponse.java +++ b/src/main/java/org/prebid/server/proto/response/CookieSyncResponse.java @@ -1,13 +1,11 @@ package org.prebid.server.proto.response; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.cookie.model.CookieSyncStatus; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class CookieSyncResponse { CookieSyncStatus status; diff --git a/src/main/java/org/prebid/server/proto/response/VideoResponse.java b/src/main/java/org/prebid/server/proto/response/VideoResponse.java index 364f5a4beac..4eb942bd9f8 100644 --- a/src/main/java/org/prebid/server/proto/response/VideoResponse.java +++ b/src/main/java/org/prebid/server/proto/response/VideoResponse.java @@ -14,4 +14,3 @@ public class VideoResponse { ExtAmpVideoResponse ext; } - diff --git a/src/main/java/org/prebid/server/protobuf/response/ProtobufResponseUtils.java b/src/main/java/org/prebid/server/protobuf/response/ProtobufResponseUtils.java index 3f1e38f757b..65b272d5281 100644 --- a/src/main/java/org/prebid/server/protobuf/response/ProtobufResponseUtils.java +++ b/src/main/java/org/prebid/server/protobuf/response/ProtobufResponseUtils.java @@ -79,7 +79,7 @@ public static ProtobufMapper extensionMapper) { return (OpenRtb.NativeResponse.Asset asset) -> - com.iab.openrtb.response.Asset.builder() + Asset.builder() .id(asset.getId()) .required(BooleanUtils.toInteger(asset.getRequired())) .title(titleMapper.map(asset.getTitle())) diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java index 8a010991b2f..2f3ca855668 100644 --- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java @@ -1,57 +1,40 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; import java.util.Map; import java.util.Set; -/** - * Defines the contract of getting application settings (account, stored ad unit configurations and - * stored requests and imps) from the source. - * - * @see FileApplicationSettings - * @see JdbcApplicationSettings - * @see HttpApplicationSettings - * @see CachingApplicationSettings - * @see CompositeApplicationSettings - */ public interface ApplicationSettings { - /** - * Returns {@link Account} for the given account ID. - */ Future getAccountById(String accountId, Timeout timeout); - /** - * Fetches stored requests and imps by IDs. - */ - Future getStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout); - - /** - * Fetches AMP stored requests and imps by IDs. - */ - Future getAmpStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout); - - /** - * Fetches Video stored requests and imps by IDs. - */ - Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout); - - /** - * Fetches stored response by IDs. - */ - Future getStoredResponses(Set responseIds, Timeout timeout); + Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout); + + Future getStoredResponses(Set responseIds, Timeout timeout); - /** - * Fetches video category - */ Future> getCategories(String primaryAdServer, String publisher, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/settings/CacheNotificationListener.java b/src/main/java/org/prebid/server/settings/CacheNotificationListener.java index e8650dec1b8..5a5e8e88d75 100644 --- a/src/main/java/org/prebid/server/settings/CacheNotificationListener.java +++ b/src/main/java/org/prebid/server/settings/CacheNotificationListener.java @@ -3,9 +3,9 @@ import java.util.List; import java.util.Map; -public interface CacheNotificationListener { +public interface CacheNotificationListener { - void save(Map requests, Map imps); + void save(Map requests, Map imps); void invalidate(List requests, List imps); } diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java index 297c19ac2af..3a39d9eae40 100644 --- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java @@ -1,16 +1,17 @@ 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.execution.timeout.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; import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredItem; import org.prebid.server.settings.model.StoredResponseDataResult; @@ -24,9 +25,6 @@ import java.util.function.BiFunction; import java.util.function.Consumer; -/** - * Adds caching functionality for {@link ApplicationSettings} implementation. - */ public class CachingApplicationSettings implements ApplicationSettings { private static final Logger logger = LoggerFactory.getLogger(CachingApplicationSettings.class); @@ -37,99 +35,52 @@ public class CachingApplicationSettings implements ApplicationSettings { private final Map accountToErrorCache; private final Map adServerPublisherToErrorCache; private final Map> categoryConfigCache; - private final SettingsCache cache; - private final SettingsCache ampCache; - private final SettingsCache videoCache; + private final SettingsCache cache; + private final SettingsCache ampCache; + private final SettingsCache videoCache; + private final SettingsCache profileCache; private final Metrics metrics; public CachingApplicationSettings(ApplicationSettings delegate, - SettingsCache cache, - SettingsCache ampCache, - SettingsCache videoCache, + SettingsCache cache, + SettingsCache ampCache, + SettingsCache videoCache, + SettingsCache profileCache, 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); + this.profileCache = Objects.requireNonNull(profileCache); this.metrics = Objects.requireNonNull(metrics); } - /** - * Retrieves account from cache or delegates it to original fetcher. - */ @Override 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)); } - /** - * Retrieves stored data from cache or delegates it to original fetcher. - */ - @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); - } - - /** - * Retrieves amp stored data from cache or delegates it to original fetcher. - */ - @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); - } - - @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { - - return getFromCacheOrDelegate(videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); - } - - /** - * Delegates stored response retrieve to original fetcher, as caching is not supported fot stored response. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return delegate.getStoredResponses(responseIds, timeout); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - final String compoundKey = StringUtils.isNotBlank(publisher) - ? "%s_%s".formatted(primaryAdServer, publisher) - : primaryAdServer; - - return getFromCacheOrDelegate(categoryConfigCache, adServerPublisherToErrorCache, compoundKey, timeout, - (key, timeoutParam) -> delegate.getCategories(primaryAdServer, publisher, timeout), - CachingApplicationSettings::noOp); - } - private static Future getFromCacheOrDelegate(Map cache, Map accountToErrorCache, String key, @@ -159,79 +110,115 @@ private static Future getFromCacheOrDelegate(Map cache, .recover(throwable -> cacheAndReturnFailedFuture(throwable, key, accountToErrorCache)); } - /** - * Retrieves stored data from cache and collects ids which were absent. For absent ids makes look up to original - * source, combines results and updates cache with missed stored item. In case when origin source returns failed - * {@link Future} propagates its result to caller. In successive call return {@link Future<StoredDataResult>} - * with all found stored items and error from origin source id call was made. - */ - private static Future getFromCacheOrDelegate( - SettingsCache cache, - String accountId, - Set requestIds, - Set impIds, - Timeout timeout, - StoredDataFetcher, Set, Timeout, Future> retriever) { + private static Future cacheAndReturnFailedFuture(Throwable throwable, + String key, + Map cache) { + + if (throwable instanceof PreBidException) { + cache.put(key, throwable.getMessage()); + } + + return Future.failedFuture(throwable); + } + + @Override + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate(cache, accountId, requestIds, impIds, timeout, delegate::getStoredData); + } + + @Override + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + ampCache, accountId, requestIds, impIds, timeout, delegate::getAmpStoredData); + } + + @Override + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + videoCache, accountId, requestIds, impIds, timeout, delegate::getVideoStoredData); + } + + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataFromCacheOrDelegate( + profileCache, accountId, requestIds, impIds, timeout, delegate::getProfiles); + } + + private static Future> getStoredDataFromCacheOrDelegate(SettingsCache cache, + String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever) { // empty string account ID doesn't make sense final String normalizedAccountId = StringUtils.stripToNull(accountId); - // search in cache - final Map> requestCache = cache.getRequestCache(); - final Map> impCache = cache.getImpCache(); + final Map>> requestCache = cache.getRequestCache(); + final Map>> impCache = cache.getImpCache(); final Set missedRequestIds = new HashSet<>(); - final Map storedIdToRequest = getFromCacheOrAddMissedIds(normalizedAccountId, requestIds, - requestCache, missedRequestIds); + final Map storedIdToRequest = getFromCacheOrAddMissedIds( + normalizedAccountId, requestIds, requestCache, missedRequestIds); final Set missedImpIds = new HashSet<>(); - final Map storedIdToImp = getFromCacheOrAddMissedIds(normalizedAccountId, impIds, impCache, - missedImpIds); + final Map storedIdToImp = getFromCacheOrAddMissedIds( + normalizedAccountId, impIds, impCache, missedImpIds); if (missedRequestIds.isEmpty() && missedImpIds.isEmpty()) { return Future.succeededFuture( - StoredDataResult.of(storedIdToRequest, storedIdToImp, Collections.emptyList())); + StoredDataResult.of( + Collections.unmodifiableMap(storedIdToRequest), + Collections.unmodifiableMap(storedIdToImp), + Collections.emptyList())); } - // delegate call to original source for missed ids and update cache with it return retriever.apply(normalizedAccountId, missedRequestIds, missedImpIds, timeout).map(result -> { - final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); + final Map storedIdToRequestFromDelegate = result.getStoredIdToRequest(); storedIdToRequest.putAll(storedIdToRequestFromDelegate); - for (Map.Entry entry : storedIdToRequestFromDelegate.entrySet()) { + for (Map.Entry entry : storedIdToRequestFromDelegate.entrySet()) { cache.saveRequestCache(normalizedAccountId, entry.getKey(), entry.getValue()); } - final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); + final Map storedIdToImpFromDelegate = result.getStoredIdToImp(); storedIdToImp.putAll(storedIdToImpFromDelegate); - for (Map.Entry entry : storedIdToImpFromDelegate.entrySet()) { + for (Map.Entry entry : storedIdToImpFromDelegate.entrySet()) { cache.saveImpCache(normalizedAccountId, entry.getKey(), entry.getValue()); } - return StoredDataResult.of(storedIdToRequest, storedIdToImp, result.getErrors()); + return StoredDataResult.of( + Collections.unmodifiableMap(storedIdToRequest), + Collections.unmodifiableMap(storedIdToImp), + result.getErrors()); }); } - private static Future cacheAndReturnFailedFuture(Throwable throwable, - String key, - Map cache) { + private static Map getFromCacheOrAddMissedIds(String accountId, + Set ids, + Map>> cache, + Set missedIds) { - if (throwable instanceof PreBidException) { - cache.put(key, throwable.getMessage()); - } - - return Future.failedFuture(throwable); - } - - private static Map getFromCacheOrAddMissedIds(String accountId, - Set ids, - Map> cache, - Set missedIds) { - - final Map idToStoredItem = new HashMap<>(ids.size()); + final Map idToStoredItem = new HashMap<>(ids.size()); for (String id : ids) { try { - final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); + final StoredItem resolvedStoredItem = StoredItemResolver.resolve(null, accountId, id, cache.get(id)); idToStoredItem.put(id, resolvedStoredItem.getData()); } catch (PreBidException e) { missedIds.add(id); @@ -241,14 +228,30 @@ private static Map getFromCacheOrAddMissedIds(String accountId, return idToStoredItem; } - public void invalidateAccountCache(String accountId) { - accountCache.remove(accountId); - logger.debug("Account with id {0} was invalidated", accountId); + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); } - public void invalidateAllAccountCache() { - accountCache.clear(); - logger.debug("All accounts cache were invalidated"); + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + final String compoundKey = StringUtils.isNotBlank(publisher) + ? "%s_%s".formatted(primaryAdServer, publisher) + : primaryAdServer; + + return getFromCacheOrDelegate( + categoryConfigCache, + adServerPublisherToErrorCache, + compoundKey, + timeout, + (key, timeoutParam) -> delegate.getCategories(primaryAdServer, publisher, timeout), + CachingApplicationSettings::noOp); + } + + public void invalidateAccountCache(String accountId) { + accountCache.remove(accountId); + 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/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java index 2edd16b7345..225171508b4 100644 --- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java @@ -1,13 +1,13 @@ package org.prebid.server.settings; import io.vertx.core.Future; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.helper.StoredDataFetcher; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredResponseDataResult; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -15,11 +15,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiFunction; -/** - * Implements composite pattern for a list of {@link ApplicationSettings}. - */ public class CompositeApplicationSettings implements ApplicationSettings { private final Proxy proxy; @@ -42,58 +38,57 @@ private static Proxy createProxy(List delegates) { return proxy; } - /** - * Runs a process to get account by id from a chain of retrievers - * and returns {@link Future<{@link Account}>}. - */ @Override public Future getAccountById(String accountId, Timeout timeout) { return proxy.getAccountById(accountId, timeout); } - /** - * Runs a process to get stored requests by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>}. - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + return proxy.getStoredData(accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored requests by a collection of amp ids from a chain of retrievers - * and returns {@link Future<{@link StoredDataResult }>}. - */ @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return proxy.getAmpStoredData(accountId, requestIds, Collections.emptySet(), timeout); + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return proxy.getAmpStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + return proxy.getVideoStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return proxy.getCategories(primaryAdServer, publisher, timeout); + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return proxy.getProfiles(accountId, requestIds, impIds, timeout); } - /** - * Runs a process to get stored responses by a collection of ids from a chain of retrievers - * and returns {@link Future<{@link StoredResponseDataResult }>}. - */ @Override public Future getStoredResponses(Set responseIds, Timeout timeout) { return proxy.getStoredResponses(responseIds, timeout); } - /** - * Decorates {@link ApplicationSettings} for a chain of retrievers. - */ + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return proxy.getCategories(primaryAdServer, publisher, timeout); + } + private static class Proxy implements ApplicationSettings { private final ApplicationSettings applicationSettings; @@ -106,105 +101,138 @@ private Proxy(ApplicationSettings applicationSettings, Proxy next) { @Override public Future getAccountById(String accountId, Timeout timeout) { - return getConfig(accountId, timeout, applicationSettings::getAccountById, - next != null ? next::getAccountById : null); - } - - private static Future getConfig(String key, Timeout timeout, - BiFunction> retriever, - BiFunction> nextRetriever) { - return retriever.apply(key, timeout) - .recover(throwable -> nextRetriever != null - ? nextRetriever.apply(key, timeout) - : Future.failedFuture(throwable)); - } - - @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return applicationSettings.getCategories(primaryAdServer, publisher, timeout) + return applicationSettings.getAccountById(accountId, timeout) .recover(throwable -> next != null - ? next.getCategories(primaryAdServer, publisher, timeout) + ? next.getAccountById(accountId, timeout) : Future.failedFuture(throwable)); } @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return getStoredRequests(accountId, requestIds, impIds, timeout, applicationSettings::getStoredData, + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getStoredData, next != null ? next::getStoredData : null); } @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return getStoredRequests(accountId, requestIds, Collections.emptySet(), timeout, + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, applicationSettings::getAmpStoredData, next != null ? next::getAmpStoredData : null); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return getStoredRequests(accountId, requestIds, impIds, timeout, - applicationSettings::getVideoStoredData, next != null ? next::getVideoStoredData : null); - } - - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return getStoredResponses(responseIds, timeout, applicationSettings::getStoredResponses, - next != null ? next::getStoredResponses : null); - } + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { - private static Future getStoredResponses( - Set responseIds, Timeout timeout, - BiFunction, Timeout, Future> retriever, - BiFunction, Timeout, Future> nextRetriever) { - - return retriever.apply(responseIds, timeout) - .compose(retrieverResult -> - nextRetriever == null || retrieverResult.getErrors().isEmpty() - ? Future.succeededFuture(retrieverResult) - : getRemainingStoredResponses(responseIds, timeout, - retrieverResult.getIdToStoredResponses(), nextRetriever)); + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getVideoStoredData, + next != null ? next::getVideoStoredData : null); } - private static Future getStoredRequests( - String accountId, Set requestIds, Set impIds, Timeout timeout, - StoredDataFetcher, Set, Timeout, Future> retriever, - StoredDataFetcher, Set, Timeout, Future> nextRetriever) { + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredDataOrDelegate( + accountId, + requestIds, + impIds, + timeout, + applicationSettings::getProfiles, + next != null ? next::getProfiles : null); + } + + private static Future> getStoredDataOrDelegate(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + StoredDataFetcher retriever, + StoredDataFetcher nextRetriever) { return retriever.apply(accountId, requestIds, impIds, timeout) - .compose(retrieverResult -> - nextRetriever == null || retrieverResult.getErrors().isEmpty() - ? Future.succeededFuture(retrieverResult) - : getRemainingStoredRequests(accountId, requestIds, impIds, timeout, - retrieverResult.getStoredIdToRequest(), retrieverResult.getStoredIdToImp(), - nextRetriever)); - } - - private static Future getRemainingStoredRequests( - String accountId, Set requestIds, Set impIds, Timeout timeout, - Map storedIdToRequest, Map storedIdToImp, - StoredDataFetcher, Set, Timeout, Future> retriever) { - - return retriever.apply(accountId, subtractSets(requestIds, storedIdToRequest.keySet()), - subtractSets(impIds, storedIdToImp.keySet()), timeout) + .compose(retrieverResult -> nextRetriever == null || retrieverResult.getErrors().isEmpty() + ? Future.succeededFuture(retrieverResult) + : getRemainingStoredData( + accountId, + requestIds, + impIds, + timeout, + retrieverResult.getStoredIdToRequest(), + retrieverResult.getStoredIdToImp(), + nextRetriever)); + } + + private static Future> getRemainingStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout, + Map storedIdToRequest, + Map storedIdToImp, + StoredDataFetcher retriever) { + + return retriever.apply( + accountId, + subtractSets(requestIds, storedIdToRequest.keySet()), + subtractSets(impIds, storedIdToImp.keySet()), + timeout) .map(result -> StoredDataResult.of( combineMaps(storedIdToRequest, result.getStoredIdToRequest()), combineMaps(storedIdToImp, result.getStoredIdToImp()), result.getErrors())); } - private static Future getRemainingStoredResponses( - Set responseIds, Timeout timeout, Map storedSeatBids, - BiFunction, Timeout, Future> retriever) { + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return applicationSettings.getStoredResponses(responseIds, timeout) + .compose(result -> next == null || result.getErrors().isEmpty() + ? Future.succeededFuture(result) + : getRemainingStoredResponses(responseIds, timeout, result.getIdToStoredResponses())); + } + + private Future getRemainingStoredResponses( + Set responseIds, + Timeout timeout, + Map storedSeatBids) { - return retriever.apply(subtractSets(responseIds, storedSeatBids.keySet()), timeout) + return next.getStoredResponses(subtractSets(responseIds, storedSeatBids.keySet()), timeout) .map(result -> StoredResponseDataResult.of( combineMaps(storedSeatBids, result.getIdToStoredResponses()), result.getErrors())); } + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return applicationSettings.getCategories(primaryAdServer, publisher, timeout) + .recover(throwable -> next != null + ? next.getCategories(primaryAdServer, publisher, timeout) + : Future.failedFuture(throwable)); + } + private static Set subtractSets(Set first, Set second) { final Set remaining = new HashSet<>(first); remaining.removeAll(second); 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..54ffc7a425c --- /dev/null +++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java @@ -0,0 +1,250 @@ +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.Timeout; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.helper.DatabaseProfilesResultMapper; +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.Profile; +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; + +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 profiles by ids, for example: + *
+     * SELECT accountId, profileId, profile, mergePrecedence, type
+     *   FROM profiles
+     *   WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)
+     * 
+ */ + private final String selectProfilesQuery; + + /** + * 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 selectProfilesQuery, + 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.selectProfilesQuery = selectProfilesQuery; + this.selectStoredResponsesQuery = Objects.requireNonNull(selectStoredResponsesQuery); + } + + @Override + public Future getAccountById(String accountId, Timeout timeout) { + return databaseClient.executeQuery( + selectAccountQuery, + Collections.singletonList(accountId), + result -> mapToModelOrError(result, this::toAccount), + timeout) + .compose(result -> result != null + ? Future.succeededFuture(result) + : Future.failedFuture(new PreBidException("Account not found: " + accountId))); + } + + /** + * Note: mapper should never throw 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; + } + + 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()); + } + } + + @Override + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return fetchStoredData( + selectStoredRequestsQuery, + requestIds, + impIds, + result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + @Override + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return fetchStoredData( + selectAmpStoredRequestsQuery, + requestIds, + Collections.emptySet(), + result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + @Override + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return fetchStoredData( + selectStoredRequestsQuery, + requestIds, + impIds, + result -> DatabaseStoredDataResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + // TODO: remove in PBS 4.0 + if (selectProfilesQuery == null) { + return Future.failedFuture("Profiles storage not configured."); + } + + return fetchStoredData( + selectProfilesQuery, + requestIds, + impIds, + result -> DatabaseProfilesResultMapper.map(result, accountId, requestIds, impIds), + timeout); + } + + private Future> fetchStoredData(String query, + Set requestIds, + Set impIds, + Function, StoredDataResult> mapper, + Timeout timeout) { + + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + + 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()); + + return databaseClient.executeQuery(parametrizedQuery, idsQueryParameters, mapper, timeout); + } + + @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); + } + + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return Future.failedFuture(new PreBidException("Not supported")); + } +} diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java index 403583ad70f..5d1875c742c 100644 --- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java @@ -1,122 +1,138 @@ 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.prebid.server.execution.Timeout; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.execution.timeout.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.Profile; 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); + } + + 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 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); } - @Override - public Future getStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + private Account mergeAccounts(Account account) { + return defaultAccount == null + ? account + : jsonMerger.merge(account, defaultAccount, Account.class); + } - return delegate.getStoredData(accountId, requestIds, impIds, timeout); + private AccountPriceFloorsConfig extractDefaultPriceFloors() { + return Optional.ofNullable(defaultAccount) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceFloors) + .orElse(null); } - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return delegate.getStoredResponses(responseIds, timeout); + private Future recoverIfNeeded(Throwable throwable, String accountId) { + // In case of invalid account return failed future + return enforceValidAccount + ? Future.failedFuture(throwable) + : Future.succeededFuture(mergeAccounts(Account.empty(accountId))); } @Override - public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { - return delegate.getCategories(primaryAdServer, publisher, timeout); + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return delegate.getStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getAmpStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return delegate.getAmpStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, - Set requestIds, - Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { return delegate.getVideoStoredData(accountId, requestIds, impIds, timeout); } - private Account mergeAccounts(Account account) { - return jsonMerger.merge(account, defaultAccount, Account.class); - } + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { - 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 delegate.getProfiles(accountId, requestIds, impIds, timeout); + } - return account; + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + return delegate.getStoredResponses(responseIds, timeout); } - 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); + @Override + public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { + return delegate.getCategories(primaryAdServer, publisher, timeout); } } diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java index 33a1ea36390..b5930b0123f 100644 --- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java @@ -6,22 +6,27 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.file.FileSystem; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.SetUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.helper.StoredItemResolver; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.Category; +import org.prebid.server.settings.model.Profile; import org.prebid.server.settings.model.SettingsFile; 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.settings.model.StoredResponseDataResult; import java.io.File; import java.io.IOException; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -48,68 +53,237 @@ public class FileApplicationSettings implements ApplicationSettings { private final Map accounts; private final Map storedIdToRequest; private final Map storedIdToImp; + private final Map>> profileIdToProfile; private final Map storedIdToSeatBid; private final Map> fileToCategories; - public FileApplicationSettings(FileSystem fileSystem, String settingsFileName, String storedRequestsDir, - String storedImpsDir, String storedResponsesDir, String categoriesDir, + public FileApplicationSettings(FileSystem fileSystem, + String settingsFileName, + String storedRequestsDir, + String storedImpsDir, + String profilesDir, + String storedResponsesDir, + String categoriesDir, JacksonMapper jacksonMapper) { - final SettingsFile settingsFile = readSettingsFile(Objects.requireNonNull(fileSystem), + final SettingsFile settingsFile = readSettingsFile( + Objects.requireNonNull(fileSystem), Objects.requireNonNull(settingsFileName)); - accounts = toMap(settingsFile.getAccounts(), + accounts = toMap( + settingsFile.getAccounts(), Account::getId, Function.identity()); - this.storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); - this.storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); - this.storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); - this.fileToCategories = readCategories(fileSystem, Objects.requireNonNull(categoriesDir), jacksonMapper); + storedIdToRequest = readStoredData(fileSystem, Objects.requireNonNull(storedRequestsDir)); + storedIdToImp = readStoredData(fileSystem, Objects.requireNonNull(storedImpsDir)); + profileIdToProfile = profilesDir != null // TODO: require in PBS 4.0 + ? readProfiles(fileSystem, Objects.requireNonNull(profilesDir), jacksonMapper) + : Collections.emptyMap(); + storedIdToSeatBid = readStoredData(fileSystem, Objects.requireNonNull(storedResponsesDir)); + fileToCategories = readCategories(fileSystem, Objects.requireNonNull(categoriesDir), jacksonMapper); + } + + private static SettingsFile readSettingsFile(FileSystem fileSystem, String fileName) { + final Buffer buf = fileSystem.readFileBlocking(fileName); + try { + return new YAMLMapper().readValue(buf.getBytes(), SettingsFile.class); + } catch (IOException e) { + throw new IllegalArgumentException("Couldn't read file settings", e); + } + } + + private static Map toMap(List list, Function keyMapper, Function valueMapper) { + return list != null + ? list.stream().collect(Collectors.toMap(keyMapper, valueMapper)) + : Collections.emptyMap(); + } + + private static Map readStoredData(FileSystem fileSystem, String dir) { + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .collect(Collectors.toMap( + filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), + filepath -> fileSystem.readFileBlocking(filepath).toString())); + } + + private static Map>> readProfiles(FileSystem fileSystem, + String dir, + JacksonMapper jacksonMapper) { + + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .map(filepath -> readProfile(fileSystem, filepath, jacksonMapper)) + .collect(Collectors.groupingBy( + Map.Entry::getKey, + Collectors.mapping(Map.Entry::getValue, Collectors.toSet()))); + } + + private static Map.Entry> readProfile(FileSystem fileSystem, + String profileFilePath, + JacksonMapper jacksonMapper) { + + final String profileFileName = StringUtils.removeEnd(new File(profileFilePath).getName(), JSON_SUFFIX); + final String[] accountIdAndProfileId = profileFileName.split("-"); + if (accountIdAndProfileId.length != 2) { + throw new IllegalArgumentException("Invalid name of profile file: " + profileFileName); + } + + final String profileAsString = fileSystem.readFileBlocking(profileFilePath).toString(); + final Profile profile = jacksonMapper.decodeValue(profileAsString, Profile.class); + + return Map.entry(accountIdAndProfileId[1], StoredItem.of(accountIdAndProfileId[0], profile)); + } + + private static Map> readCategories(FileSystem fileSystem, + String dir, + JacksonMapper jacksonMapper) { + + return fileSystem.readDirBlocking(dir).stream() + .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) + .collect(Collectors.toMap( + filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), + filepath -> parseCategories(filepath, fileSystem.readFileBlocking(filepath), jacksonMapper))); + } + + private static Map parseCategories(String filepath, + Buffer categoriesBuffer, + JacksonMapper jacksonMapper) { + + try { + return jacksonMapper.decodeValue(categoriesBuffer, CATEGORY_FORMAT_REFERENCE); + } catch (DecodeException e) { + throw new PreBidException("Failed to decode categories for file " + filepath); + } } @Override public Future getAccountById(String accountId, Timeout timeout) { - return mapValueToFuture(accounts, accountId, "Account"); + final Account account = accounts.get(accountId); + return account != null + ? Future.succeededFuture(account) + : Future.failedFuture(new PreBidException("Account not found: " + accountId)); } - /** - * Creates {@link StoredDataResult} by checking if any ids are missed in storedRequest map - * and adding an error to list for each missed Id - * and returns {@link Future<{@link StoredDataResult }>} with all loaded files and errors list. - */ @Override - public Future getStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return Future.succeededFuture(CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds) - ? StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()) - : StoredDataResult.of( - existingStoredIdToJson(requestIds, storedIdToRequest), - existingStoredIdToJson(impIds, storedIdToImp), - Stream.of( - errorsForMissedIds(requestIds, storedIdToRequest, StoredDataType.request), - errorsForMissedIds(impIds, storedIdToImp, StoredDataType.imp)) - .flatMap(Collection::stream) + public Future> getStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + + final Map storedRequests = existingStoredIdToJson(requestIds, storedIdToRequest); + final Map storedImps = existingStoredIdToJson(impIds, storedIdToImp); + + return Future.succeededFuture(StoredDataResult.of( + storedRequests, + storedImps, + Stream.concat( + errorsForMissedIds(requestIds, storedRequests.keySet(), StoredDataType.request.name()), + errorsForMissedIds(impIds, storedImps.keySet(), StoredDataType.imp.name())) .toList())); } @Override - public Future getAmpStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { - return getStoredData(accountId, requestIds, Collections.emptySet(), timeout); + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); } @Override - public Future getVideoStoredData(String accountId, Set requestIds, Set impIds, - Timeout timeout) { + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + return getStoredData(accountId, requestIds, impIds, timeout); } + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) { + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyList())); + } + + final List errors = new ArrayList<>(); + final Map requestProfiles = getProfiles(accountId, requestIds, Profile.Type.REQUEST, errors); + final Map impProfiles = getProfiles(accountId, impIds, Profile.Type.IMP, errors); + + return Future.succeededFuture(StoredDataResult.of( + requestProfiles, + impProfiles, + Collections.unmodifiableList(errors))); + } + + private Map getProfiles(String accountId, + Set ids, + Profile.Type type, + List errors) { + + final Map result = new HashMap<>(); + + for (String id : ids) { + final Set> profiles = profilesOfTypeWithId(type, id); + + try { + final StoredItem profile = StoredItemResolver.resolve("profile", accountId, id, profiles); + result.put(id, profile.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + + return Collections.unmodifiableMap(result); + } + + private Set> profilesOfTypeWithId(Profile.Type type, String id) { + final Set> allProfiles = profileIdToProfile.get(id); + if (CollectionUtils.isEmpty(allProfiles) + || allProfiles.stream().allMatch(storedItem -> storedItem.getData().getType() == type)) { + + return allProfiles; + } + + return allProfiles.stream() + .filter(storedItem -> storedItem.getData().getType() == type) + .collect(Collectors.toSet()); + } + + @Override + public Future getStoredResponses(Set responseIds, Timeout timeout) { + if (CollectionUtils.isEmpty(responseIds)) { + return Future.succeededFuture(StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList())); + } + + final Map storedResponses = existingStoredIdToJson(responseIds, storedIdToSeatBid); + + return Future.succeededFuture(StoredResponseDataResult.of( + storedResponses, + errorsForMissedIds(responseIds, storedResponses.keySet(), StoredDataType.seatbid.name()).toList())); + } + @Override public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) { final String filename = StringUtils.isNotBlank(publisher) ? "%s_%s".formatted(primaryAdServer, publisher) : primaryAdServer; + final Map categoryToId = fileToCategories.get(filename); return categoryToId != null ? Future.succeededFuture(extractCategoriesIds(categoryToId)) @@ -120,104 +294,21 @@ public Future> getCategories(String primaryAdServer, String private static Map extractCategoriesIds(Map categoryToId) { return categoryToId.entrySet().stream() .filter(catToCategory -> catToCategory.getValue() != null) - .collect(Collectors.toMap(Map.Entry::getKey, + .collect(Collectors.toMap( + Map.Entry::getKey, catToCategory -> catToCategory.getValue().getId())); } - /** - * Creates {@link StoredResponseDataResult} by checking if any ids are missed in storedResponse map - * and adding an error to list for each missed Id - * and returns {@link Future<{@link StoredResponseDataResult }>} with all loaded files and errors list. - */ - @Override - public Future getStoredResponses(Set responseIds, Timeout timeout) { - return Future.succeededFuture(CollectionUtils.isEmpty(responseIds) - ? StoredResponseDataResult.of(Collections.emptyMap(), Collections.emptyList()) - : StoredResponseDataResult.of( - existingStoredIdToJson(responseIds, storedIdToSeatBid), - errorsForMissedIds(responseIds, storedIdToSeatBid, StoredDataType.seatbid))); - } - - private static Map toMap(List list, Function keyMapper, Function valueMapper) { - return list != null ? list.stream().collect(Collectors.toMap(keyMapper, valueMapper)) : Collections.emptyMap(); - } - - /** - * Reading YAML settings file. - */ - private static SettingsFile readSettingsFile(FileSystem fileSystem, String fileName) { - final Buffer buf = fileSystem.readFileBlocking(fileName); - try { - return new YAMLMapper().readValue(buf.getBytes(), SettingsFile.class); - } catch (IOException e) { - throw new IllegalArgumentException("Couldn't read file settings", e); - } - } - - /** - * Reads files with .json extension in configured directory and creates {@link Map} where key is a file name - * without .json extension and value is file content. - */ - private static Map readStoredData(FileSystem fileSystem, String dir) { - return fileSystem.readDirBlocking(dir).stream() - .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) - .collect(Collectors.toMap(filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), - filename -> fileSystem.readFileBlocking(filename).toString())); - } - - /** - * Reads files with .json extension in configured directory and creates {@link Map} where key is a file name - * without .json and value is file content parsed to a {@link Map} where key is category and value is - * {@link Category}. - */ - private static Map> readCategories(FileSystem fileSystem, String dir, - JacksonMapper jacksonMapper) { - return fileSystem.readDirBlocking(dir).stream() - .filter(filepath -> filepath.endsWith(JSON_SUFFIX)) - .collect(Collectors.toMap(filepath -> StringUtils.removeEnd(new File(filepath).getName(), JSON_SUFFIX), - filename -> parseCategories(filename, fileSystem.readFileBlocking(filename), jacksonMapper))); - } - - /** - * Parses {@link Buffer} to a {@link Map} where key is category and value {@link Category}. - */ - private static Map parseCategories(String fileName, Buffer categoriesBuffer, - JacksonMapper jacksonMapper) { - try { - return jacksonMapper.decodeValue(categoriesBuffer, CATEGORY_FORMAT_REFERENCE); - } catch (DecodeException e) { - throw new PreBidException("Failed to decode categories for file " + fileName); - } - } - - private static Future mapValueToFuture(Map map, String id, String errorPrefix) { - final T value = map.get(id); - return value != null - ? Future.succeededFuture(value) - : Future.failedFuture(new PreBidException("%s not found: %s".formatted(errorPrefix, id))); - } - - /** - * Returns corresponding stored id with json. - */ private static Map existingStoredIdToJson(Set requestedIds, Map storedIdToJson) { + return requestedIds.stream() .filter(storedIdToJson::containsKey) .collect(Collectors.toMap(Function.identity(), storedIdToJson::get)); } - /** - * Returns errors for missed IDs. - */ - private static List errorsForMissedIds(Set ids, Map storedIdToJson, - StoredDataType type) { - final List missedIds = ids.stream() - .filter(id -> !storedIdToJson.containsKey(id)) - .toList(); - - return missedIds.isEmpty() ? Collections.emptyList() : missedIds.stream() - .map(id -> "No stored %s found for id: %s".formatted(type, id)) - .toList(); + private static Stream errorsForMissedIds(Set requestedIds, Set foundIds, String type) { + return SetUtils.difference(requestedIds, foundIds).stream() + .map(id -> "No stored %s found for id: %s".formatted(type, id)); } } diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java index 262cceda164..c0d899508c4 100644 --- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java +++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java @@ -5,26 +5,29 @@ 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; +import org.apache.http.client.utils.URIBuilder; import org.prebid.server.exception.PreBidException; -import org.prebid.server.execution.Timeout; +import org.prebid.server.execution.timeout.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.Profile; import org.prebid.server.settings.model.StoredDataResult; import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredResponseDataResult; 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.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -35,6 +38,7 @@ import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Implementation of {@link ApplicationSettings}. @@ -44,10 +48,14 @@ * In order to enable caching and reduce latency for read operations {@link HttpApplicationSettings} * can be decorated by {@link CachingApplicationSettings}. *

- * Expected the endpoint to satisfy the following API: + * Expected the endpoint to satisfy the following API (URL is encoded): *

* GET {endpoint}?request-ids=["req1","req2"]&imp-ids=["imp1","imp2","imp3"] *

+ * or settings.http.rfc3986-compatible is set to true + *

+ * * GET {endpoint}?request-id=req1&request-id=req2&imp-id=imp1&imp-id=imp2&imp-id=imp3 + * *

* This endpoint should return a payload like: *

  * {
@@ -70,6 +78,7 @@ public class HttpApplicationSettings implements ApplicationSettings {
             new TypeReference<>() {
             };
 
+    private final boolean isRfc3986Compatible;
     private final String endpoint;
     private final String ampEndpoint;
     private final String videoEndpoint;
@@ -77,19 +86,25 @@ public class HttpApplicationSettings implements ApplicationSettings {
     private final HttpClient httpClient;
     private final JacksonMapper mapper;
 
-    public HttpApplicationSettings(HttpClient httpClient, JacksonMapper mapper, String endpoint, String ampEndpoint,
-                                   String videoEndpoint, String categoryEndpoint) {
+    public HttpApplicationSettings(boolean isRfc3986Compatible,
+                                   String endpoint,
+                                   String ampEndpoint,
+                                   String videoEndpoint,
+                                   String categoryEndpoint,
+                                   HttpClient httpClient,
+                                   JacksonMapper mapper) {
+
+        this.isRfc3986Compatible = isRfc3986Compatible;
+        this.endpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(endpoint));
+        this.ampEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(ampEndpoint));
+        this.videoEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(videoEndpoint));
+        this.categoryEndpoint = HttpUtil.validateUrlSyntax(Objects.requireNonNull(categoryEndpoint));
         this.httpClient = Objects.requireNonNull(httpClient);
         this.mapper = Objects.requireNonNull(mapper);
-        this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint));
-        this.ampEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(ampEndpoint));
-        this.videoEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(videoEndpoint));
-        this.categoryEndpoint = HttpUtil.validateUrl(Objects.requireNonNull(categoryEndpoint));
     }
 
     @Override
     public Future getAccountById(String accountId, Timeout timeout) {
-
         return fetchAccountsByIds(Collections.singleton(accountId), timeout)
                 .map(accounts -> accounts.stream()
                         .findFirst()
@@ -101,33 +116,34 @@ private Future> fetchAccountsByIds(Set accountIds, Timeout
         if (CollectionUtils.isEmpty(accountIds)) {
             return Future.succeededFuture(Collections.emptySet());
         }
+
         final long remainingTimeout = timeout.remaining();
         if (timeout.remaining() <= 0) {
             return Future.failedFuture(new TimeoutException("Timeout has been exceeded"));
         }
 
         return httpClient.get(accountsRequestUrlFrom(endpoint, accountIds), HttpUtil.headers(), remainingTimeout)
-                .compose(response -> processAccountsResponse(response, accountIds))
-                .recover(Future::failedFuture);
+                .map(response -> processAccountsResponse(response, accountIds));
     }
 
-    private static String accountsRequestUrlFrom(String endpoint, Set accountIds) {
-        final StringBuilder url = new StringBuilder(endpoint);
-        url.append(endpoint.contains("?") ? "&" : "?");
-
-        if (!accountIds.isEmpty()) {
-            url.append("account-ids=[\"").append(joinIds(accountIds)).append("\"]");
+    private String accountsRequestUrlFrom(String endpoint, Set accountIds) {
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(endpoint);
+            if (!accountIds.isEmpty()) {
+                if (isRfc3986Compatible) {
+                    accountIds.forEach(accountId -> uriBuilder.addParameter("account-id", accountId));
+                } else {
+                    uriBuilder.addParameter("account-ids", "[\"%s\"]".formatted(joinIds(accountIds)));
+                }
+            }
+            return uriBuilder.build().toString();
+        } catch (URISyntaxException e) {
+            throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
         }
-
-        return url.toString();
-    }
-
-    private Future> processAccountsResponse(HttpClientResponse response, Set accountIds) {
-        return Future.succeededFuture(
-                toAccountsResult(response.getStatusCode(), response.getBody(), accountIds));
     }
 
-    private Set toAccountsResult(int statusCode, String body, Set accountIds) {
+    private Set processAccountsResponse(HttpClientResponse httpClientResponse, Set accountIds) {
+        final int statusCode = httpClientResponse.getStatusCode();
         if (statusCode != HttpResponseStatus.OK.code()) {
             throw new PreBidException("Error fetching accounts %s via http: unexpected response status %d"
                     .formatted(accountIds, statusCode));
@@ -135,7 +151,7 @@ private Set toAccountsResult(int statusCode, String body, Set a
 
         final HttpAccountsResponse response;
         try {
-            response = mapper.decodeValue(body, HttpAccountsResponse.class);
+            response = mapper.decodeValue(httpClientResponse.getBody(), HttpAccountsResponse.class);
         } catch (DecodeException e) {
             throw new PreBidException("Error fetching accounts %s via http: failed to parse response: %s"
                     .formatted(accountIds, e.getMessage()));
@@ -145,86 +161,38 @@ private Set toAccountsResult(int statusCode, String body, Set a
         return MapUtils.isNotEmpty(accounts) ? new HashSet<>(accounts.values()) : Collections.emptySet();
     }
 
-    /**
-     * Runs a process to get stored requests by a collection of ids from http service
-     * and returns {@link Future<{@link StoredDataResult }>}
-     */
     @Override
-    public Future getStoredData(String accountId, Set requestIds, Set impIds,
-                                                  Timeout timeout) {
-        return fetchStoredData(endpoint, requestIds, impIds, timeout);
-    }
+    public Future> getStoredData(String accountId,
+                                                          Set requestIds,
+                                                          Set impIds,
+                                                          Timeout timeout) {
 
-    /**
-     * Runs a process to get stored requests by a collection of amp ids from http service
-     * and returns {@link Future<{@link StoredDataResult }>}
-     */
-    @Override
-    public Future getAmpStoredData(String accountId, Set requestIds, Set impIds,
-                                                     Timeout timeout) {
-        return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout);
+        return fetchStoredData(endpoint, requestIds, impIds, timeout);
     }
 
-    /**
-     * Not supported and returns failed result.
-     */
     @Override
-    public Future getVideoStoredData(String accountId, Set requestIds, Set impIds,
-                                                       Timeout timeout) {
-        return fetchStoredData(videoEndpoint, requestIds, impIds, timeout);
-    }
+    public Future> getAmpStoredData(String accountId,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
-    /**
-     * Not supported and returns failed result.
-     */
-    @Override
-    public Future getStoredResponses(Set responseIds, Timeout timeout) {
-        return Future.failedFuture(new PreBidException("Not supported"));
+        return fetchStoredData(ampEndpoint, requestIds, Collections.emptySet(), timeout);
     }
 
     @Override
-    public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) {
-        final String url = StringUtils.isNotEmpty(publisher)
-                ? "%s/%s/%s.json".formatted(categoryEndpoint, primaryAdServer, publisher)
-                : "%s/%s.json".formatted(categoryEndpoint, primaryAdServer);
-        final long remainingTimeout = timeout.remaining();
-        if (remainingTimeout <= 0) {
-            return Future.failedFuture(new TimeoutException(
-                    "Failed to fetch categories from url '%s'. Reason: Timeout exceeded".formatted(url)));
-        }
-        return httpClient.get(url, remainingTimeout)
-                .map(httpClientResponse -> processCategoryResponse(httpClientResponse, url));
-    }
-
-    private Map processCategoryResponse(HttpClientResponse httpClientResponse, String url) {
-        final int statusCode = httpClientResponse.getStatusCode();
-        if (statusCode != 200) {
-            throw makeFailedCategoryFetchException(url, "Response status code is '%d'".formatted(statusCode));
-        }
-
-        final String body = httpClientResponse.getBody();
-        if (StringUtils.isEmpty(body)) {
-            throw makeFailedCategoryFetchException(url, "Response body is null or empty");
-        }
+    public Future> getVideoStoredData(String accountId,
+                                                               Set requestIds,
+                                                               Set impIds,
+                                                               Timeout timeout) {
 
-        final Map categories;
-        try {
-            categories = mapper.decodeValue(body, CATEGORY_RESPONSE_REFERENCE);
-        } catch (DecodeException e) {
-            throw makeFailedCategoryFetchException(url, "Failed to decode response body with error " + e.getMessage());
-        }
-        return categories.entrySet().stream()
-                .filter(catToCategory -> catToCategory.getValue() != null)
-                .collect(Collectors.toMap(Map.Entry::getKey,
-                        catToCategory -> catToCategory.getValue().getId()));
+        return fetchStoredData(videoEndpoint, requestIds, impIds, timeout);
     }
 
-    private PreBidException makeFailedCategoryFetchException(String url, String reason) {
-        return new PreBidException("Failed to fetch categories from url '%s'. Reason: %s".formatted(url, reason));
-    }
+    private Future> fetchStoredData(String endpoint,
+                                                             Set requestIds,
+                                                             Set impIds,
+                                                             Timeout timeout) {
 
-    private Future fetchStoredData(String endpoint, Set requestIds, Set impIds,
-                                                     Timeout timeout) {
         if (CollectionUtils.isEmpty(requestIds) && CollectionUtils.isEmpty(impIds)) {
             return Future.succeededFuture(
                     StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList()));
@@ -236,64 +204,66 @@ private Future fetchStoredData(String endpoint, Set re
         }
 
         return httpClient.get(storeRequestUrlFrom(endpoint, requestIds, impIds), HttpUtil.headers(), remainingTimeout)
-                .compose(response -> processStoredDataResponse(response, requestIds, impIds))
+                .map(response -> processStoredDataResponse(response, requestIds, impIds))
                 .recover(exception -> failStoredDataResponse(exception, requestIds, impIds));
     }
 
-    private static String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) {
-        final StringBuilder url = new StringBuilder(endpoint);
-        url.append(endpoint.contains("?") ? "&" : "?");
-
-        if (!requestIds.isEmpty()) {
-            url.append("request-ids=[\"").append(joinIds(requestIds)).append("\"]");
-        }
-
-        if (!impIds.isEmpty()) {
-            if (!requestIds.isEmpty()) {
-                url.append("&");
-            }
-            url.append("imp-ids=[\"").append(joinIds(impIds)).append("\"]");
-        }
-
-        return url.toString();
-    }
-
-    private static String joinIds(Set ids) {
-        return String.join("\",\"", ids);
-    }
+    private static Future> failStoredDataResponse(Throwable throwable,
+                                                                           Set requestIds,
+                                                                           Set impIds) {
 
-    private static Future failStoredDataResponse(Throwable throwable, Set requestIds,
-                                                                   Set impIds) {
-        return Future.succeededFuture(
-                toFailedStoredDataResult(requestIds, impIds, throwable.getMessage()));
+        return Future.succeededFuture(toFailedStoredDataResult(requestIds, impIds, throwable.getMessage()));
     }
 
-    private Future processStoredDataResponse(HttpClientResponse response, Set requestIds,
-                                                               Set impIds) {
-        return Future.succeededFuture(
-                toStoredDataResult(requestIds, impIds, response.getStatusCode(), response.getBody()));
-    }
+    private static StoredDataResult toFailedStoredDataResult(Set requestIds,
+                                                                     Set impIds,
+                                                                     String errorMessageFormat,
+                                                                     Object... args) {
 
-    private static StoredDataResult toFailedStoredDataResult(Set requestIds, Set impIds,
-                                                             String errorMessageFormat, Object... args) {
-        final String errorRequests = requestIds.isEmpty() ? ""
-                : "stored requests for ids " + requestIds;
+        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;
 
         final String error = "Error fetching %s%s%s via HTTP: %s"
                 .formatted(errorRequests, separator, errorImps, errorMessageFormat.formatted(args));
-
         logger.info(error);
+
         return StoredDataResult.of(Collections.emptyMap(), Collections.emptyMap(), Collections.singletonList(error));
     }
 
-    private StoredDataResult toStoredDataResult(Set requestIds, Set impIds,
-                                                int statusCode, String body) {
+    private String storeRequestUrlFrom(String endpoint, Set requestIds, Set impIds) {
+        try {
+            final URIBuilder uriBuilder = new URIBuilder(endpoint);
+            if (!requestIds.isEmpty()) {
+                if (isRfc3986Compatible) {
+                    requestIds.forEach(requestId -> uriBuilder.addParameter("request-id", requestId));
+                } else {
+                    uriBuilder.addParameter("request-ids", "[\"%s\"]".formatted(joinIds(requestIds)));
+                }
+            }
+            if (!impIds.isEmpty()) {
+                if (isRfc3986Compatible) {
+                    impIds.forEach(impId -> uriBuilder.addParameter("imp-id", impId));
+                } else {
+                    uriBuilder.addParameter("imp-ids", "[\"%s\"]".formatted(joinIds(impIds)));
+                }
+            }
+            return uriBuilder.build().toString();
+        } catch (URISyntaxException e) {
+            throw new PreBidException("URL %s has bad syntax".formatted(endpoint));
+        }
+    }
+
+    private StoredDataResult processStoredDataResponse(HttpClientResponse httpClientResponse,
+                                                               Set requestIds,
+                                                               Set impIds) {
+
+        final int statusCode = httpClientResponse.getStatusCode();
         if (statusCode != HttpResponseStatus.OK.code()) {
             return toFailedStoredDataResult(requestIds, impIds, "HTTP status code %d", statusCode);
         }
 
+        final String body = httpClientResponse.getBody();
         final HttpFetcherResponse response;
         try {
             response = mapper.decodeValue(body, HttpFetcherResponse.class);
@@ -305,8 +275,10 @@ private StoredDataResult toStoredDataResult(Set requestIds, Set
         return parseResponse(requestIds, impIds, response);
     }
 
-    private StoredDataResult parseResponse(Set requestIds, Set impIds,
-                                           HttpFetcherResponse response) {
+    private StoredDataResult parseResponse(Set requestIds,
+                                                   Set impIds,
+                                                   HttpFetcherResponse response) {
+
         final List errors = new ArrayList<>();
 
         final Map storedIdToRequest =
@@ -318,10 +290,12 @@ private StoredDataResult parseResponse(Set requestIds, Set impId
         return StoredDataResult.of(storedIdToRequest, storedIdToImp, errors);
     }
 
-    private Map parseStoredDataOrAddError(Set ids, Map storedData,
-                                                          StoredDataType type, List errors) {
+    private Map parseStoredDataOrAddError(Set ids,
+                                                          Map storedData,
+                                                          StoredDataType type,
+                                                          List errors) {
+
         final Map result = new HashMap<>(ids.size());
-        final Set notParsedIds = new HashSet<>();
 
         if (storedData != null) {
             for (Map.Entry entry : storedData.entrySet()) {
@@ -332,7 +306,6 @@ private Map parseStoredDataOrAddError(Set ids, Map parseStoredDataOrAddError(Set ids, Map missedIds = new HashSet<>(ids);
             missedIds.removeAll(result.keySet());
-            missedIds.removeAll(notParsedIds);
 
-            errors.addAll(missedIds.stream()
-                    .map(id -> "Stored %s not found for id: %s".formatted(type, id))
-                    .toList());
+            missedIds.forEach(id -> errors.add("Stored %s not found for id: %s".formatted(type, id)));
         }
 
         return result;
     }
+
+    @Override
+    public Future> getProfiles(String accountId,
+                                                         Set requestIds,
+                                                         Set impIds,
+                                                         Timeout timeout) {
+
+        return Future.succeededFuture(StoredDataResult.of(
+                Collections.emptyMap(),
+                Collections.emptyMap(),
+                Stream.concat(requestIds.stream(), impIds.stream())
+                        .map(id -> "Profile not found for id: " + id)
+                        .toList()));
+    }
+
+    @Override
+    public Future getStoredResponses(Set responseIds, Timeout timeout) {
+        return Future.failedFuture(new PreBidException("Not supported"));
+    }
+
+    @Override
+    public Future> getCategories(String primaryAdServer, String publisher, Timeout timeout) {
+        final String url = StringUtils.isNotEmpty(publisher)
+                ? "%s/%s/%s.json".formatted(categoryEndpoint, primaryAdServer, publisher)
+                : "%s/%s.json".formatted(categoryEndpoint, primaryAdServer);
+
+        final long remainingTimeout = timeout.remaining();
+        if (remainingTimeout <= 0) {
+            return Future.failedFuture(new TimeoutException(
+                    "Failed to fetch categories from url '%s'. Reason: Timeout exceeded".formatted(url)));
+        }
+
+        return httpClient.get(url, remainingTimeout)
+                .map(httpClientResponse -> processCategoryResponse(httpClientResponse, url));
+    }
+
+    private Map processCategoryResponse(HttpClientResponse httpClientResponse, String url) {
+        final int statusCode = httpClientResponse.getStatusCode();
+        if (statusCode != 200) {
+            throw makeFailedCategoryFetchException(url, "Response status code is '%d'".formatted(statusCode));
+        }
+
+        final String body = httpClientResponse.getBody();
+        if (StringUtils.isEmpty(body)) {
+            throw makeFailedCategoryFetchException(url, "Response body is null or empty");
+        }
+
+        final Map categories;
+        try {
+            categories = mapper.decodeValue(body, CATEGORY_RESPONSE_REFERENCE);
+        } catch (DecodeException e) {
+            throw makeFailedCategoryFetchException(url, "Failed to decode response body with error " + e.getMessage());
+        }
+
+        return categories.entrySet().stream()
+                .filter(catToCategory -> catToCategory.getValue() != null)
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        catToCategory -> catToCategory.getValue().getId()));
+    }
+
+    private PreBidException makeFailedCategoryFetchException(String url, String reason) {
+        return new PreBidException("Failed to fetch categories from url '%s'. Reason: %s".formatted(url, reason));
+    }
+
+    private static String joinIds(Set ids) {
+        return String.join("\",\"", ids);
+    }
 }
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..c9b71c6e7ad --- /dev/null +++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java @@ -0,0 +1,242 @@ +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.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.Profile; +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); + } + + @Override + public Future> getAmpStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future> getVideoStoredData(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return getStoredData(accountId, requestIds, impIds, timeout); + } + + @Override + public Future> getProfiles(String accountId, + Set requestIds, + Set impIds, + Timeout timeout) { + + return Future.succeededFuture(StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Stream.concat(requestIds.stream(), impIds.stream()) + .map(id -> "Profile not found for id: " + id) + .toList())); + } + + @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(); + } + + private Set missingStoredDataIds(Map fileContents, Set requestedIds) { + return SetUtils.difference(requestedIds, fileContents.keySet()); + } +} diff --git a/src/main/java/org/prebid/server/settings/SettingsCache.java b/src/main/java/org/prebid/server/settings/SettingsCache.java index 1bba204db36..1066d0a121c 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,51 +12,63 @@ 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. */ -public class SettingsCache implements CacheNotificationListener { +public class SettingsCache implements CacheNotificationListener { - private final Map> requestCache; - private final Map> impCache; + 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(); } - Map> getRequestCache() { + Map>> getRequestCache() { return requestCache; } - Map> getImpCache() { + Map>> getImpCache() { return impCache; } - void saveRequestCache(String accountId, String requestId, String requestValue) { - saveCachedValue(requestCache, accountId, requestId, requestValue); + void saveRequestCache(String accountId, String requestId, T value) { + saveCachedValue(requestCache, accountId, requestId, value); } - void saveImpCache(String accountId, String impId, String impValue) { - saveCachedValue(impCache, accountId, impId, impValue); + void saveImpCache(String accountId, String impId, T value) { + saveCachedValue(impCache, accountId, impId, value); } - private static void saveCachedValue(Map> cache, - String accountId, String id, String value) { - final Set values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>()); + private static void saveCachedValue(Map>> cache, + String accountId, + String id, + T value) { + + final Set> values = ObjectUtils.defaultIfNull(cache.get(id), new HashSet<>()); values.add(StoredItem.of(accountId, value)); cache.put(id, values); } @@ -65,7 +79,7 @@ private static void saveCachedValue(Map> cache, * TODO: account should be added to all services uses this method */ @Override - public void save(Map requests, Map imps) { + public void save(Map requests, Map imps) { if (MapUtils.isNotEmpty(requests)) { requests.forEach((key, value) -> requestCache.put(key, Collections.singleton(StoredItem.of(null, value)))); } @@ -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/DatabaseProfilesResultMapper.java b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java new file mode 100644 index 00000000000..65471ef30ab --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseProfilesResultMapper.java @@ -0,0 +1,176 @@ +package org.prebid.server.settings.helper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.settings.model.Profile; +import org.prebid.server.settings.model.StoredDataResult; +import org.prebid.server.settings.model.StoredItem; +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.Objects; +import java.util.Set; + +public class DatabaseProfilesResultMapper { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseProfilesResultMapper.class); + + private DatabaseProfilesResultMapper() { + } + + public static StoredDataResult map(RowSet resultSet) { + return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + } + + /** + * Note: mapper should never throw exception in case of using + * {@link CircuitBreakerSecuredDatabaseClient}. + */ + public static StoredDataResult map(RowSet rowSet, + String accountId, + Set requestIds, + Set impIds) { + + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + final List errors = new ArrayList<>(); + + if (rowIterator == null || !rowIterator.hasNext()) { + handleEmptyResult(requestIds, impIds, errors); + + return StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.unmodifiableList(errors)); + } + + final Map>> requestIdToProfiles = new HashMap<>(); + final Map>> impIdToProfiles = new HashMap<>(); + + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.size() < 5) { + final String message = "Error occurred while mapping profiles: some columns are missing"; + logger.error(message); + errors.add(message); + + return StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.unmodifiableList(errors)); + } + + final String fetchedAccountId = Objects.toString(row.getValue(0), null); + final String id = Objects.toString(row.getValue(1), null); + final String profileBodyAsString = Objects.toString(row.getValue(2), StringUtils.EMPTY); + final String mergePrecedenceAsString = Objects.toString(row.getValue(3), null); + final String typeAsString = Objects.toString(row.getValue(4), StringUtils.EMPTY); + + final JsonNode profileBody; + final Profile.MergePrecedence mergePrecedence; + final Profile.Type type; + try { + profileBody = ObjectMapperProvider.mapper().readTree(profileBodyAsString); + mergePrecedence = mergePrecedenceAsString != null + ? Profile.MergePrecedence.valueOf(mergePrecedenceAsString.toUpperCase()) + : Profile.MergePrecedence.REQUEST; + type = Profile.Type.valueOf(typeAsString.toUpperCase()); + } catch (IllegalArgumentException e) { + logger.error("Profile with id={} has invalid value: type={}, mergePrecedence={} and will be ignored.", + e, id, typeAsString, mergePrecedenceAsString); + continue; + } catch (JsonProcessingException e) { + logger.error("Profile with id={} has invalid body: ''{}'' and will be ignored.", + e, id, profileBodyAsString); + continue; + } + + final Profile profile = Profile.of(type, mergePrecedence, profileBody); + + if (type == Profile.Type.REQUEST) { + addStoredItem(fetchedAccountId, id, profile, requestIdToProfiles); + } else { + addStoredItem(fetchedAccountId, id, profile, impIdToProfiles); + } + } + + return StoredDataResult.of( + storedItemsOrAddError( + accountId, + requestIds, + requestIdToProfiles, + errors), + storedItemsOrAddError( + accountId, + impIds, + impIdToProfiles, + errors), + Collections.unmodifiableList(errors)); + } + + private static void handleEmptyResult(Set requestIds, Set impIds, List errors) { + if (requestIds.isEmpty() && impIds.isEmpty()) { + errors.add("No profiles were found"); + } else { + final String errorRequests = requestIds.isEmpty() + ? "" + : "request profiles for ids " + requestIds; + final String separator = requestIds.isEmpty() || impIds.isEmpty() ? "" : " and "; + final String errorImps = impIds.isEmpty() ? "" : "imp profiles for ids " + impIds; + + errors.add("No %s%s%s were found".formatted(errorRequests, separator, errorImps)); + } + } + + private static void addStoredItem(String accountId, + String id, + Profile profile, + Map>> idToStoredItems) { + + idToStoredItems.computeIfAbsent(id, key -> new HashSet<>()).add(StoredItem.of(accountId, profile)); + } + + private static Map storedItemsOrAddError( + String accountId, + Set searchIds, + Map>> foundIdToStoredItems, + List errors) { + + final Map result = new HashMap<>(); + + if (searchIds.isEmpty()) { + foundIdToStoredItems.forEach((id, storedItems) -> { + for (StoredItem storedItem : storedItems) { + result.put(id, storedItem.getData()); + } + }); + + return Collections.unmodifiableMap(result); + } + + for (String id : searchIds) { + try { + final StoredItem resolvedStoredItem = StoredItemResolver + .resolve("profile", accountId, id, foundIdToStoredItems.get(id)); + + result.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + + return Collections.unmodifiableMap(result); + } +} 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..6e37850a903 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredDataResultMapper.java @@ -0,0 +1,164 @@ +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.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.Objects; +import java.util.Set; + +public class DatabaseStoredDataResultMapper { + + private static final Logger logger = LoggerFactory.getLogger(DatabaseStoredDataResultMapper.class); + + private DatabaseStoredDataResultMapper() { + } + + /** + * Overloaded method for cases when no specific IDs are required, e.g. fetching all records. + */ + public static StoredDataResult map(RowSet resultSet) { + return map(resultSet, null, Collections.emptySet(), Collections.emptySet()); + } + + /** + * Note: mapper should never throw exception in case of using + * {@link CircuitBreakerSecuredDatabaseClient}. + */ + public static StoredDataResult map(RowSet rowSet, + String accountId, + Set requestIds, + Set impIds) { + + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + final List errors = new ArrayList<>(); + + if (rowIterator == null || !rowIterator.hasNext()) { + handleEmptyResult(requestIds, impIds, errors); + + return StoredDataResult.of( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.unmodifiableList(errors)); + } + + final Map>> requestIdToStoredItems = new HashMap<>(); + final Map>> impIdToStoredItems = new HashMap<>(); + + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.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(), + Collections.unmodifiableList(errors)); + } + + final String fetchedAccountId = Objects.toString(row.getValue(0), null); + final String id = Objects.toString(row.getValue(1), null); + final String data = Objects.toString(row.getValue(2), null); + final String typeAsString = Objects.toString(row.getValue(3), null); + + 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); + } + } + + return StoredDataResult.of( + storedItemsOrAddError( + StoredDataType.request, + accountId, + requestIds, + requestIdToStoredItems, + errors), + storedItemsOrAddError( + StoredDataType.imp, + accountId, + impIds, + impIdToStoredItems, + errors), + Collections.unmodifiableList(errors)); + } + + private static void handleEmptyResult(Set requestIds, Set impIds, List errors) { + 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)); + } + } + + private static void addStoredItem(String accountId, + String id, + String data, + Map>> idToStoredItems) { + + idToStoredItems.computeIfAbsent(id, key -> new HashSet<>()).add(StoredItem.of(accountId, data)); + } + + private static Map storedItemsOrAddError(StoredDataType type, + String accountId, + Set searchIds, + Map>> foundIdToStoredItems, + List errors) { + + final Map result = new HashMap<>(); + + if (searchIds.isEmpty()) { + foundIdToStoredItems.forEach((id, storedItems) -> { + for (StoredItem storedItem : storedItems) { + result.put(id, storedItem.getData()); + } + }); + + return Collections.unmodifiableMap(result); + } + + for (String id : searchIds) { + try { + final StoredItem resolvedStoredItem = StoredItemResolver + .resolve("stored " + type.toString(), accountId, id, foundIdToStoredItems.get(id)); + + result.put(id, resolvedStoredItem.getData()); + } catch (PreBidException e) { + errors.add(e.getMessage()); + } + } + + return Collections.unmodifiableMap(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..b3812804cfb --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/DatabaseStoredResponseResultMapper.java @@ -0,0 +1,60 @@ +package org.prebid.server.settings.helper; + +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import org.apache.commons.collections4.SetUtils; +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.Objects; +import java.util.Set; + +public class DatabaseStoredResponseResultMapper { + + private DatabaseStoredResponseResultMapper() { + } + + public static StoredResponseDataResult map(RowSet rowSet, Set responseIds) { + final RowIterator rowIterator = rowSet != null ? rowSet.iterator() : null; + final List errors = new ArrayList<>(); + + if (rowIterator == null || !rowIterator.hasNext()) { + handleEmptyResult(responseIds, errors); + return StoredResponseDataResult.of(Collections.emptyMap(), Collections.unmodifiableList(errors)); + } + + final Map storedIdToResponse = new HashMap<>(responseIds.size()); + + while (rowIterator.hasNext()) { + final Row row = rowIterator.next(); + if (row.size() < 2) { + errors.add("Result set column number is less than expected"); + return StoredResponseDataResult.of(Collections.emptyMap(), Collections.unmodifiableList(errors)); + } + + storedIdToResponse.put( + Objects.toString(row.getValue(0), null), + Objects.toString(row.getValue(1), null)); + } + + SetUtils.difference(responseIds, storedIdToResponse.keySet()) + .forEach(id -> errors.add("No stored response found for id: " + id)); + + return StoredResponseDataResult.of( + Collections.unmodifiableMap(storedIdToResponse), + Collections.unmodifiableList(errors)); + } + + private static void handleEmptyResult(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..64d79db1663 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryHelper.java @@ -0,0 +1,15 @@ +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 replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber); + + String replaceStoredResponseIdPlaceholders(String query, int idsNumber); +} 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..e492b1ce385 --- /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 replaceRequestAndImpIdPlaceholders(String query, int requestIdNumber, int impIdNumber) { + return query + .replace(REQUEST_ID_PLACEHOLDER, parameterHolders(requestIdNumber)) + .replace(IMP_ID_PLACEHOLDER, parameterHolders(impIdNumber)); + } + + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replace(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber)); + } + + 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..69388e3423c --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/ParametrizedQueryPostgresHelper.java @@ -0,0 +1,46 @@ +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 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(); + } + + @Override + public String replaceStoredResponseIdPlaceholders(String query, int idsNumber) { + return query.replaceAll(RESPONSE_ID_PLACEHOLDER, parameterHolders(idsNumber, 0)); + } + + 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/helper/StoredDataFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java index f173bcff45e..4a0feeeb2bb 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredDataFetcher.java @@ -1,18 +1,13 @@ package org.prebid.server.settings.helper; +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.settings.model.StoredDataResult; -/** - * Interface to satisfy obtaining of {@link StoredDataResult}. - * - * @param account ID - * @param set of stored request IDs - * @param set of stored imp IDs - * @param processing timeout - * @param result of fetching stored data - */ +import java.util.Set; + @FunctionalInterface -public interface StoredDataFetcher { +public interface StoredDataFetcher { - R apply(ACC account, REQS reqIds, IMPS impIds, T timeout); + Future> apply(String account, Set reqIds, Set impIds, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java index 0729141bd30..d0797a4772f 100644 --- a/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java +++ b/src/main/java/org/prebid/server/settings/helper/StoredItemResolver.java @@ -3,7 +3,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.exception.PreBidException; -import org.prebid.server.settings.model.StoredDataType; import org.prebid.server.settings.model.StoredItem; import java.util.Objects; @@ -26,15 +25,15 @@ private StoredItemResolver() { *

* - Otherwise, find stored item for this account or report an error if no one account matched. *

- * 2. One stored stored item was found: + * 2. One stored item was found: *

* - If account is not specified in stored item or found stored item has the same account - use it. *

* - Otherwise, reject stored item as if there hadn't been match. */ - public static StoredItem resolve(StoredDataType type, String accountId, String id, Set storedItems) { + public static StoredItem resolve(String type, String accountId, String id, Set> storedItems) { if (CollectionUtils.isEmpty(storedItems)) { - throw new PreBidException("No stored %s found for id: %s".formatted(type, id)); + throw new PreBidException("No %s found for id: %s".formatted(type, id)); } // at least one stored item has account @@ -42,21 +41,25 @@ public static StoredItem resolve(StoredDataType type, String accountId, String i if (StringUtils.isEmpty(accountId)) { // we cannot choose stored item among multiple without account throw new PreBidException( - "Multiple stored %ss found for id: %s but no account was specified".formatted(type, id)); + "Multiple %ss found for id: %s but no account was specified".formatted(type, id)); } + return storedItems.stream() .filter(storedItem -> Objects.equals(storedItem.getAccountId(), accountId)) .findAny() .orElseThrow(() -> new PreBidException( - "No stored %s found among multiple id: %s for account: %s".formatted(type, id, accountId))); + "No %s found among multiple id: %s for account: %s".formatted(type, id, accountId))); } // only one stored item found - final StoredItem storedItem = storedItems.iterator().next(); - if (StringUtils.isBlank(accountId) || storedItem.getAccountId() == null + final StoredItem storedItem = storedItems.iterator().next(); + if (StringUtils.isBlank(accountId) + || storedItem.getAccountId() == null || Objects.equals(accountId, storedItem.getAccountId())) { + return storedItem; } - throw new PreBidException("No stored %s found for id: %s for account: %s".formatted(type, id, accountId)); + + throw new PreBidException("No %s found for id: %s for account: %s".formatted(type, id, accountId)); } } diff --git a/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java b/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java new file mode 100644 index 00000000000..3092841b06e --- /dev/null +++ b/src/main/java/org/prebid/server/settings/helper/StoredResponseFetcher.java @@ -0,0 +1,13 @@ +package org.prebid.server.settings.helper; + +import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.StoredResponseDataResult; + +import java.util.Set; + +@FunctionalInterface +public interface StoredResponseFetcher { + + Future apply(Set responseIds, Timeout timeout); +} 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..2aa8c974ffa 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,19 @@ public class Account { AccountMetricsConfig metrics; - @JsonProperty("cookie-sync") + @JsonAlias("cookie-sync") AccountCookieSyncConfig cookieSync; AccountHooksConfiguration hooks; + AccountSettings settings; + + @JsonAlias("alternate-bidder-codes") + AccountAlternateBidderCodes alternateBidderCodes; + + AccountVtrackConfig vtrack; + 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/AccountAlternateBidderCodes.java b/src/main/java/org/prebid/server/settings/model/AccountAlternateBidderCodes.java new file mode 100644 index 00000000000..bcf8e937eb8 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAlternateBidderCodes.java @@ -0,0 +1,14 @@ +package org.prebid.server.settings.model; + +import lombok.Value; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; + +import java.util.Map; + +@Value(staticConstructor = "of") +public class AccountAlternateBidderCodes implements AlternateBidderCodesConfig { + + Boolean enabled; + + Map bidders; +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountAlternateBidderCodesBidder.java b/src/main/java/org/prebid/server/settings/model/AccountAlternateBidderCodesBidder.java new file mode 100644 index 00000000000..50f230133d5 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAlternateBidderCodesBidder.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Value; +import org.prebid.server.auction.aliases.AlternateBidder; + +import java.util.Set; + +@Value(staticConstructor = "of") +public class AccountAlternateBidderCodesBidder implements AlternateBidder { + + Boolean enabled; + + @JsonAlias("allowed-bidder-codes") + Set allowedBidderCodes; +} 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/AccountAuctionBidRoundingMode.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java new file mode 100644 index 00000000000..839602540aa --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java @@ -0,0 +1,20 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AccountAuctionBidRoundingMode { + + @JsonProperty("down") + @JsonEnumDefaultValue + DOWN, + + @JsonProperty("true") + TRUE, + + @JsonProperty("timesplit") + TIMESPLIT, + + @JsonProperty("up") + UP +} 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..c78bd14770c 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -1,8 +1,11 @@ package org.prebid.server.settings.model; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import lombok.Value; +import org.prebid.server.auction.model.PaaFormat; import org.prebid.server.spring.config.bidder.model.MediaType; import java.util.Map; @@ -11,37 +14,55 @@ @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; + @JsonProperty("bidadjustments") + ObjectNode bidAdjustments; + AccountEventsConfig events; - @JsonProperty("price-floors") + @JsonAlias("price-floors") AccountPriceFloorsConfig priceFloors; AccountTargetingConfig targeting; + @JsonAlias("bid-rounding") + AccountAuctionBidRoundingMode bidRounding; + @JsonProperty("preferredmediatype") Map preferredMediaTypes; @JsonProperty("privacysandbox") AccountPrivacySandboxConfig privacySandbox; + + @JsonProperty("paaformat") + PaaFormat paaFormat; + + AccountCacheConfig cache; + + AccountBidRankingConfig ranking; + + @JsonAlias("impression-limit") + Integer impressionLimit; + + AccountProfilesConfig profiles; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java new file mode 100644 index 00000000000..361ff4c9781 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountBidRankingConfig { + + Boolean enabled; +} 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/AccountCacheConfig.java b/src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java new file mode 100644 index 00000000000..72f205865b8 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountCacheConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountCacheConfig { + + Boolean enabled; +} 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..f45af371385 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,8 +10,10 @@ @Value(staticConstructor = "of") public class AccountHooksConfiguration { - @JsonProperty("execution-plan") + @JsonAlias("execution-plan") ExecutionPlan executionPlan; Map modules; + + HooksAdminConfig admin; } 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..9acebb1c427 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,21 @@ 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; + + @JsonAlias("max-rules") + Long maxRules; + + @JsonAlias("max-schema-dims") + Long maxSchemaDims; } 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..42824b410e2 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,21 @@ 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-schema-dims") + Long maxSchemaDims; + + @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/AccountProfilesConfig.java b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java new file mode 100644 index 00000000000..73d710a072d --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountProfilesConfig.java @@ -0,0 +1,13 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountProfilesConfig { + + Integer limit; + + @JsonAlias("fail-on-unknown") + Boolean failOnUnknown; +} 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/AccountVtrackConfig.java b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java new file mode 100644 index 00000000000..95939389e05 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountVtrackConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountVtrackConfig { + + Integer ttl; +} 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..80d4abd9cfb 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 @@ -36,4 +36,3 @@ public class GdprConfig { @JsonProperty("purpose-one-treatment-interpretation") PurposeOneTreatmentInterpretation purposeOneTreatmentInterpretation; } - diff --git a/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java new file mode 100644 index 00000000000..36b12a401f0 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class HooksAdminConfig { + + @JsonAlias("module-execution") + Map moduleExecution; + +} diff --git a/src/main/java/org/prebid/server/settings/model/Profile.java b/src/main/java/org/prebid/server/settings/model/Profile.java new file mode 100644 index 00000000000..11b46fc43d1 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/Profile.java @@ -0,0 +1,48 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@Value +@Builder +@Jacksonized +public class Profile { + + Type type; + + @JsonProperty("mergeprecedence") + @Builder.Default + MergePrecedence mergePrecedence = MergePrecedence.REQUEST; + + JsonNode body; + + public static Profile of(Type type, MergePrecedence mergePrecedence, JsonNode body) { + return Profile.builder() + .type(type) + .mergePrecedence(mergePrecedence) + .body(body) + .build(); + } + + public enum Type { + + @JsonAlias("request") + REQUEST, + + @JsonAlias("imp") + IMP + } + + public enum MergePrecedence { + + @JsonAlias("request") + REQUEST, + + @JsonAlias("profile") + PROFILE + } +} 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/Purposes.java b/src/main/java/org/prebid/server/settings/model/Purposes.java index 16156e6cffb..1d0732b48c2 100644 --- a/src/main/java/org/prebid/server/settings/model/Purposes.java +++ b/src/main/java/org/prebid/server/settings/model/Purposes.java @@ -31,4 +31,3 @@ public class Purposes { Purpose p10; } - 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..17cdb19ff40 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,6 @@ 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/SpecialFeatures.java b/src/main/java/org/prebid/server/settings/model/SpecialFeatures.java index f7974e444a7..a927dbfcadf 100644 --- a/src/main/java/org/prebid/server/settings/model/SpecialFeatures.java +++ b/src/main/java/org/prebid/server/settings/model/SpecialFeatures.java @@ -15,4 +15,3 @@ public class SpecialFeatures { SpecialFeature sf2; } - diff --git a/src/main/java/org/prebid/server/settings/model/StoredDataResult.java b/src/main/java/org/prebid/server/settings/model/StoredDataResult.java index 263cf8f4300..19840586be1 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredDataResult.java +++ b/src/main/java/org/prebid/server/settings/model/StoredDataResult.java @@ -1,18 +1,16 @@ package org.prebid.server.settings.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value -public class StoredDataResult { +@Value(staticConstructor = "of") +public class StoredDataResult { - Map storedIdToRequest; + Map storedIdToRequest; - Map storedIdToImp; + Map storedIdToImp; List errors; } diff --git a/src/main/java/org/prebid/server/settings/model/StoredItem.java b/src/main/java/org/prebid/server/settings/model/StoredItem.java index 5b639a30cdd..dd7ee8095ff 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredItem.java +++ b/src/main/java/org/prebid/server/settings/model/StoredItem.java @@ -1,16 +1,14 @@ package org.prebid.server.settings.model; -import lombok.AllArgsConstructor; import lombok.Value; /** * The model helps to reduce multiple rows found for single stored request/imp ID. */ -@AllArgsConstructor(staticName = "of") -@Value -public class StoredItem { +@Value(staticConstructor = "of") +public class StoredItem { String accountId; - String data; + T data; } diff --git a/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java b/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java index ad27eda7954..bc85d5034e3 100644 --- a/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java +++ b/src/main/java/org/prebid/server/settings/model/StoredResponseDataResult.java @@ -1,13 +1,11 @@ package org.prebid.server.settings.model; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class StoredResponseDataResult { Map idToStoredResponses; diff --git a/src/main/java/org/prebid/server/settings/model/VideoStoredDataResult.java b/src/main/java/org/prebid/server/settings/model/VideoStoredDataResult.java index 186997df3ac..8838cc8a381 100644 --- a/src/main/java/org/prebid/server/settings/model/VideoStoredDataResult.java +++ b/src/main/java/org/prebid/server/settings/model/VideoStoredDataResult.java @@ -1,15 +1,13 @@ package org.prebid.server.settings.model; import com.iab.openrtb.request.Video; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Collections; import java.util.List; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class VideoStoredDataResult { private static final VideoStoredDataResult EMPTY = VideoStoredDataResult.of(Collections.emptyMap(), diff --git a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java index eea8b4f644e..98352cf6bee 100644 --- a/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java +++ b/src/main/java/org/prebid/server/settings/model/activity/privacy/AccountPrivacyModuleConfig.java @@ -23,6 +23,9 @@ public sealed interface AccountPrivacyModuleConfig permits PrivacyModuleQualifier getCode(); + @JsonProperty("skipRate") + int getSkipRate(); + @JsonProperty Boolean enabled(); } 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..92ccff4c3e9 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; @@ -16,6 +17,8 @@ public class AccountUSCustomLogicModuleConfig implements AccountPrivacyModuleCon @Accessors(fluent = true) Boolean enabled; + int skipRate; + Config config; @Override @@ -28,10 +31,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 +45,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..d4aa86f2d3c 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; @@ -13,6 +14,8 @@ public class AccountUSNatModuleConfig implements AccountPrivacyModuleConfig { @Accessors(fluent = true) Boolean enabled; + int skipRate; + Config config; @Override @@ -23,7 +26,12 @@ public PrivacyModuleQualifier getCode() { @Value(staticConstructor = "of") public static class Config { - @JsonProperty("skipSids") + @JsonProperty("skip_sids") + @JsonAlias({"skipSids", "skip-sids"}) List skipSids; + + @JsonProperty("allow_personal_data_consent_2") + @JsonAlias({"allowPersonalDataConsent2", "allow-personal-data-consent-2"}) + boolean allowPersonalDataConsent2; } } 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/proto/request/InvalidateSettingsCacheRequest.java b/src/main/java/org/prebid/server/settings/proto/request/InvalidateSettingsCacheRequest.java index 288b997e636..698fd569242 100644 --- a/src/main/java/org/prebid/server/settings/proto/request/InvalidateSettingsCacheRequest.java +++ b/src/main/java/org/prebid/server/settings/proto/request/InvalidateSettingsCacheRequest.java @@ -1,12 +1,10 @@ package org.prebid.server.settings.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.List; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class InvalidateSettingsCacheRequest { List requests; diff --git a/src/main/java/org/prebid/server/settings/proto/request/UpdateSettingsCacheRequest.java b/src/main/java/org/prebid/server/settings/proto/request/UpdateSettingsCacheRequest.java index 93c58121f5d..f1cdd231850 100644 --- a/src/main/java/org/prebid/server/settings/proto/request/UpdateSettingsCacheRequest.java +++ b/src/main/java/org/prebid/server/settings/proto/request/UpdateSettingsCacheRequest.java @@ -1,12 +1,10 @@ package org.prebid.server.settings.proto.request; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class UpdateSettingsCacheRequest { Map requests; diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java index a893b2dc01a..9770fefc163 100644 --- a/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpAccountsResponse.java @@ -1,13 +1,11 @@ package org.prebid.server.settings.proto.response; -import lombok.AllArgsConstructor; import lombok.Value; import org.prebid.server.settings.model.Account; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HttpAccountsResponse { Map accounts; diff --git a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java index 256e9d3273e..0d2f7e774e5 100644 --- a/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java +++ b/src/main/java/org/prebid/server/settings/proto/response/HttpFetcherResponse.java @@ -1,13 +1,11 @@ package org.prebid.server.settings.proto.response; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.AllArgsConstructor; import lombok.Value; import java.util.Map; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HttpFetcherResponse { Map requests; 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..7714243954d --- /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.Timeout; +import org.prebid.server.execution.timeout.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..7669074d455 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;
@@ -65,7 +66,7 @@ public class HttpPeriodicRefreshService implements Initializable {
     private final String refreshUrl;
     private final long refreshPeriod;
     private final long timeout;
-    private final CacheNotificationListener cacheNotificationListener;
+    private final CacheNotificationListener cacheNotificationListener;
     private final Vertx vertx;
     private final HttpClient httpClient;
     private final JacksonMapper mapper;
@@ -75,7 +76,7 @@ public class HttpPeriodicRefreshService implements Initializable {
     public HttpPeriodicRefreshService(String refreshUrl,
                                       long refreshPeriod,
                                       long timeout,
-                                      CacheNotificationListener cacheNotificationListener,
+                                      CacheNotificationListener cacheNotificationListener,
                                       Vertx vertx,
                                       HttpClient httpClient,
                                       JacksonMapper mapper) {
@@ -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..35ff2a6f55a
--- /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..d6561b8fe32 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; @@ -36,8 +35,11 @@ USNatGppReaderFactory usNatGppReaderFactory() { } @Bean - USNatModuleCreator usNatModuleCreator(USNatGppReaderFactory gppReaderFactory) { - return new USNatModuleCreator(gppReaderFactory); + USNatModuleCreator usNatModuleCreator(USNatGppReaderFactory gppReaderFactory, + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new USNatModuleCreator(gppReaderFactory, metrics, logSamplingRate); } } @@ -55,9 +57,16 @@ USCustomLogicModuleCreator usCustomLogicModuleCreator( JsonLogic jsonLogic, @Value("${settings.in-memory-cache.ttl-seconds:#{null}}") Integer ttlSeconds, @Value("${settings.in-memory-cache.cache-size:#{null}}") Integer cacheSize, - Metrics metrics) { - - return new USCustomLogicModuleCreator(gppReaderFactory, jsonLogic, ttlSeconds, cacheSize, metrics); + Metrics metrics, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + + return new USCustomLogicModuleCreator( + gppReaderFactory, + jsonLogic, + ttlSeconds, + cacheSize, + metrics, + logSamplingRate); } } } @@ -66,18 +75,15 @@ USCustomLogicModuleCreator usCustomLogicModuleCreator( static class RuleCreatorConfiguration { @Bean - ComponentRuleCreator componentRuleCreator() { - return new ComponentRuleCreator(); + ConditionsRuleCreator conditionsRuleCreator() { + return new ConditionsRuleCreator(); } @Bean - GeoRuleCreator geoRuleCreator() { - return new GeoRuleCreator(); - } + PrivacyModulesRuleCreator privacyModulesRuleCreator(List privacyModuleCreators, + Metrics metrics) { - @Bean - PrivacyModulesRuleCreator privacyModulesRuleCreator(List privacyModuleCreators) { - return new PrivacyModulesRuleCreator(privacyModuleCreators); + return new PrivacyModulesRuleCreator(privacyModuleCreators, metrics); } } 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..c27cbd41078 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,16 @@ 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.liveintent.LiveIntentAnalyticsReporter; +import org.prebid.server.analytics.reporter.liveintent.model.LiveIntentAnalyticsProperties; 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 +21,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 +31,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.Map; +import java.util.Set; +import java.util.stream.Collectors; @Configuration public class AnalyticsConfiguration { @@ -35,7 +49,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 +59,9 @@ AnalyticsReporterDelegator analyticsReporterDelegator( tcfEnforcement, userFpdActivityMask, metrics, - logSamplingRate); + logSamplingRate, + globalEnabledAdapters, + mapper); } @Bean @@ -52,6 +70,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 { @@ -126,4 +304,47 @@ private static class PubstackBufferProperties { Long reportTtlMs; } } + + @Configuration + @ConditionalOnProperty(prefix = "analytics.liveintent", name = "enabled", havingValue = "true") + public static class LiveIntentAnalyticsConfiguration { + + @Bean + LiveIntentAnalyticsReporter liveIntentAnalyticsReporter( + LiveIntentAnalyticsConfigurationProperties properties, + HttpClient httpClient, + JacksonMapper jacksonMapper) { + + return new LiveIntentAnalyticsReporter( + properties.toComponentProperties(), + httpClient, + jacksonMapper); + } + + @Bean + @ConfigurationProperties(prefix = "analytics.liveintent") + LiveIntentAnalyticsConfigurationProperties liveIntentAnalyticsConfigurationProperties() { + return new LiveIntentAnalyticsConfigurationProperties(); + } + + @Validated + @NoArgsConstructor + @Data + private static class LiveIntentAnalyticsConfigurationProperties { + + String partnerId; + + String analyticsEndpoint; + + long timeoutMs; + + public LiveIntentAnalyticsProperties toComponentProperties() { + return LiveIntentAnalyticsProperties.builder() + .partnerId(this.partnerId) + .analyticsEndpoint(this.analyticsEndpoint) + .timeoutMs(this.timeoutMs) + .build(); + } + } + } } 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..bfb56b8c0c1 100644 --- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java @@ -1,11 +1,12 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClientOptions; import lombok.Data; import org.apache.commons.lang3.StringUtils; -import org.prebid.server.execution.RemoteFileSyncer; -import org.prebid.server.execution.retry.FixedIntervalRetryPolicy; +import org.prebid.server.auction.GeoLocationServiceWrapper; +import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; +import org.prebid.server.execution.file.FileUtil; +import org.prebid.server.execution.file.syncer.FileSyncer; import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService; import org.prebid.server.geolocation.ConfigurationGeoLocationService; import org.prebid.server.geolocation.CountryCodeMapper; @@ -13,8 +14,8 @@ 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.HttpClientProperties; -import org.prebid.server.spring.config.model.RemoteFileSyncerProperties; +import org.prebid.server.spring.config.model.FileSyncerProperties; +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 +35,7 @@ import java.util.ArrayList; import java.util.List; +@Configuration public class GeoLocationConfiguration { @Configuration @@ -49,14 +51,14 @@ CircuitBreakerProperties maxMindCircuitBreakerProperties() { @Bean @ConfigurationProperties(prefix = "geolocation.maxmind.remote-file-syncer") - RemoteFileSyncerProperties maxMindRemoteFileSyncerProperties() { - return new RemoteFileSyncerProperties(); + FileSyncerProperties maxMindRemoteFileSyncerProperties() { + return new FileSyncerProperties(); } @Bean @ConditionalOnProperty(prefix = "geolocation.circuit-breaker", name = "enabled", havingValue = "false", matchIfMissing = true) - GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncerProperties, + GeoLocationService basicGeoLocationService(FileSyncerProperties fileSyncerProperties, Vertx vertx) { return createGeoLocationService(fileSyncerProperties, vertx); @@ -67,7 +69,7 @@ GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncer CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( Vertx vertx, Metrics metrics, - RemoteFileSyncerProperties fileSyncerProperties, + FileSyncerProperties fileSyncerProperties, @Qualifier("maxMindCircuitBreakerProperties") CircuitBreakerProperties circuitBreakerProperties, Clock clock) { @@ -77,24 +79,10 @@ CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService( circuitBreakerProperties.getClosingIntervalMs(), clock); } - private GeoLocationService createGeoLocationService(RemoteFileSyncerProperties properties, Vertx vertx) { - final HttpClientProperties httpClientProperties = properties.getHttpClient(); - final HttpClientOptions httpClientOptions = new HttpClientOptions() - .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) - .setMaxRedirects(httpClientProperties.getMaxRedirects()); - - final RemoteFileSyncer remoteFileSyncer = new RemoteFileSyncer( - properties.getDownloadUrl(), - properties.getSaveFilepath(), - properties.getTmpFilepath(), - FixedIntervalRetryPolicy.limited(properties.getRetryIntervalMs(), properties.getRetryCount()), - properties.getTimeoutMs(), - properties.getUpdateIntervalMs(), - vertx.createHttpClient(httpClientOptions), - vertx); + private GeoLocationService createGeoLocationService(FileSyncerProperties properties, Vertx vertx) { final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService(); - - remoteFileSyncer.sync(maxMindGeoLocationService); + final FileSyncer fileSyncer = FileUtil.fileSyncerFor(maxMindGeoLocationService, properties, vertx); + fileSyncer.sync(); return maxMindGeoLocationService; } } @@ -180,22 +168,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..836b45ca285 100644 --- a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java @@ -1,8 +1,8 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import io.vertx.ext.jdbc.JDBCClient; -import org.prebid.server.execution.TimeoutFactory; +import io.vertx.sqlclient.Pool; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.geolocation.GeoLocationService; import org.prebid.server.health.ApplicationChecker; import org.prebid.server.health.DatabaseHealthChecker; @@ -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/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java index bffc5ee32f0..4cbfde1ffde 100644 --- a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java @@ -3,11 +3,13 @@ import io.vertx.core.Vertx; import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.HookCatalog; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.v1.Module; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.settings.model.HooksAdminConfig; +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; @@ -15,6 +17,8 @@ import java.time.Clock; import java.util.Collection; +import java.util.Collections; +import java.util.Optional; @Configuration public class HooksConfiguration { @@ -30,16 +34,24 @@ HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfigurat TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + @Value("${settings.modules.require-config-to-invoke:false}") + boolean isConfigToInvokeRequired, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return HookStageExecutor.create( hooksConfiguration.getHostExecutionPlan(), hooksConfiguration.getDefaultAccountExecutionPlan(), + Optional.ofNullable(hooksConfiguration.getAdmin()) + .map(HooksAdminConfig::getModuleExecution) + .orElseGet(Collections::emptyMap), hookCatalog, timeoutFactory, vertx, clock, - mapper); + mapper, + isConfigToInvokeRequired, + logSamplingRate); } @Bean @@ -56,5 +68,7 @@ private static class HooksConfigurationProperties { String hostExecutionPlan; String defaultAccountExecutionPlan; + + HooksAdminConfig admin; } } 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..6da6838a65d 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -1,13 +1,16 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.execution.TimeoutFactory; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.BasicPriceFloorAdjuster; 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 +21,13 @@ 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.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.context.annotation.Primary; @Configuration public class PriceFloorsConfiguration { @@ -50,8 +55,7 @@ PriceFloorFetcher priceFloorFetcher( @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, - Metrics metrics) { + PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, Metrics metrics) { return new BasicPriceFloorEnforcer(currencyConversionService, metrics); } @@ -81,9 +85,11 @@ PriceFloorResolver noOpPriceFloorResolver() { @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") PriceFloorProcessor basicPriceFloorProcessor(PriceFloorFetcher floorFetcher, PriceFloorResolver floorResolver, - JacksonMapper mapper) { + Metrics metrics, + JacksonMapper mapper, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { - return new BasicPriceFloorProcessor(floorFetcher, floorResolver, mapper); + return new BasicPriceFloorProcessor(floorFetcher, floorResolver, metrics, mapper, logSamplingRate); } @Bean @@ -93,14 +99,32 @@ 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) { - return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); + FloorAdjustmentsResolver floorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); + } + + @Bean + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") + BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { + + return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver, floorAdjustmentsResolver); + } + + @Bean + @Primary + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") + PriceFloorAdjuster noSignalBidderPriceFloorAdjuster(BasicPriceFloorAdjuster basicPriceFloorAdjuster) { + return new NoSignalBidderPriceFloorAdjuster(basicPriceFloorAdjuster); } @Bean 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..601de9e5f11 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 @@ -392,11 +393,10 @@ CoppaEnforcement coppaEnforcement(UserFpdCoppaMask userFpdCoppaMask, Metrics met @Bean TcfEnforcement tcfEnforcement(TcfDefinerService tcfDefinerService, UserFpdTcfMask userFpdTcfMask, - BidderCatalog bidderCatalog, Metrics metrics, @Value("${lmt.enforce}") boolean lmtEnforce) { - return new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, lmtEnforce); + return new TcfEnforcement(tcfDefinerService, userFpdTcfMask, metrics, lmtEnforce); } @Data 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..64a8dc7614a 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -5,36 +5,45 @@ 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 lombok.Data; 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.StoredRequestProcessor; -import org.prebid.server.auction.StoredResponseProcessor; +import org.prebid.server.auction.SkippedAuctionService; import org.prebid.server.auction.SupplyChainResolver; import org.prebid.server.auction.TimeoutResolver; import org.prebid.server.auction.UidUpdater; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestCleaner; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestCurrencyBlocker; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestMediaFilter; +import org.prebid.server.auction.bidderrequestpostprocessor.BidderRequestPreferredMediaProcessor; +import org.prebid.server.auction.bidderrequestpostprocessor.CompositeBidderRequestPostProcessor; import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; +import org.prebid.server.auction.externalortb.ProfilesProcessor; +import org.prebid.server.auction.externalortb.StoredRequestProcessor; +import org.prebid.server.auction.externalortb.StoredResponseProcessor; import org.prebid.server.auction.gpp.AmpGppService; import org.prebid.server.auction.gpp.AuctionGppService; import org.prebid.server.auction.gpp.CookieSyncGppService; @@ -43,19 +52,13 @@ import org.prebid.server.auction.gpp.processor.GppContextProcessor; import org.prebid.server.auction.gpp.processor.tcfeuv2.TcfEuV2ContextProcessor; import org.prebid.server.auction.gpp.processor.uspv1.UspV1ContextProcessor; -import org.prebid.server.auction.mediatypeprocessor.BidderMediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.CompositeMediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; -import org.prebid.server.auction.mediatypeprocessor.MultiFormatMediaTypeProcessor; import org.prebid.server.auction.privacy.contextfactory.AmpPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory; import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory; -import org.prebid.server.auction.privacy.enforcement.ActivityEnforcement; import org.prebid.server.auction.privacy.enforcement.CcpaEnforcement; -import org.prebid.server.auction.privacy.enforcement.CoppaEnforcement; +import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcement; import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService; -import org.prebid.server.auction.privacy.enforcement.TcfEnforcement; import org.prebid.server.auction.requestfactory.AmpRequestFactory; import org.prebid.server.auction.requestfactory.AuctionRequestFactory; import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver; @@ -63,25 +66,30 @@ import org.prebid.server.auction.requestfactory.VideoRequestFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; +import org.prebid.server.bidadjustments.BidAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; +import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; +import org.prebid.server.bidadjustments.BidAdjustmentsResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; 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.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.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; @@ -91,7 +99,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,22 +111,23 @@ 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.CacheDefaultTtlProperties; import org.prebid.server.spring.config.model.ExternalConversionProperties; import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties; import org.prebid.server.spring.config.model.HttpClientProperties; 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 +138,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; @@ -138,6 +146,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -148,20 +157,15 @@ @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( - @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, + CoreCacheService cacheService( + CacheConfigurationProperties cacheConfigurationProperties, @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs, + @Value("${pbc.api.key:#{null}}") String apiKey, + @Value("${datacenter-region:#{null}}") String datacenterRegion, VastModifier vastModifier, EventsService eventsService, HttpClient httpClient, @@ -169,12 +173,26 @@ CacheService cacheService( Clock clock, JacksonMapper mapper) { - return new CacheService( - CacheTtl.of(bannerCacheTtl, videoCacheTtl), + final String scheme = cacheConfigurationProperties.getScheme(); + final String host = cacheConfigurationProperties.getHost(); + final String path = cacheConfigurationProperties.getPath(); + final String query = cacheConfigurationProperties.getQuery(); + final CacheConfigurationProperties.InternalCacheConfigurationProperties internalProperties = + cacheConfigurationProperties.getInternal(); + + return new CoreCacheService( httpClient, - CacheService.getCacheEndpointUrl(scheme, host, path), - CacheService.getCachedAssetUrlTemplate(scheme, host, path, query), + CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), + internalProperties == null ? null : CacheServiceUtil.getCacheEndpointUrl( + internalProperties.getScheme(), + internalProperties.getHost(), + internalProperties.getPath()), + CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query), expectedCacheTimeMs, + apiKey, + cacheConfigurationProperties.isApiKeySecured(), + cacheConfigurationProperties.isAppendTraceInfoToCacheId(), + datacenterRegion, vastModifier, eventsService, metrics, @@ -183,6 +201,69 @@ CacheService cacheService( mapper); } + @Bean + @ConfigurationProperties(prefix = "cache") + CacheConfigurationProperties cacheConfigurationProperties() { + return new CacheConfigurationProperties(); + } + + @Data + private static class CacheConfigurationProperties { + + private String scheme; + + private String host; + + private String path; + + private String query; + + boolean apiKeySecured; + + boolean appendTraceInfoToCacheId; + + private InternalCacheConfigurationProperties internal; + + @Data + private static class InternalCacheConfigurationProperties { + + private String scheme; + + private String host; + + private String path; + } + } + + @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, + Clock clock, + Metrics metrics) { + + return new BasicPbcStorageService( + httpClient, + CacheServiceUtil.getCacheEndpointUrl(scheme, host, path), + apiKey, + callTimeoutMs, + mapper, + clock, + metrics); + } + @Bean VastModifier vastModifier(BidderCatalog bidderCatalog, EventsService eventsService, Metrics metrics) { return new VastModifier(bidderCatalog, eventsService, metrics); @@ -226,6 +307,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 +328,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 +352,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 +369,11 @@ Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver( cacheOnlyWinningBids, generateBidRequestId, adServerCurrency, - splitToList(blacklistedAppsString), + splitToList(blocklistedAppsString), externalUrl, hostVendorId, datacenterRegion, + bidderCatalog, implicitParametersExtractor, timeoutResolver, ipAddressHelper, @@ -358,45 +432,39 @@ 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, TimeoutResolver auctionTimeoutResolver, TimeoutFactory timeoutFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, 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, auctionTimeoutResolver, timeoutFactory, storedRequestProcessor, + profilesProcessor, applicationSettings, ipAddressHelper, hookStageExecutor, - userAdditionalInfoService, - priceFloorProcessor, countryCodeMapper, - metrics, - clock); + metrics); } @Bean @@ -404,6 +472,7 @@ AuctionRequestFactory auctionRequestFactory( @Value("${auction.max-request-size}") @Min(0) int maxRequestSize, Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, AuctionGppService auctionGppService, CookieDeprecationService cookieDeprecationService, @@ -412,12 +481,15 @@ AuctionRequestFactory auctionRequestFactory( OrtbTypesResolver ortbTypesResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper, + BidAdjustmentsEnricher bidAdjustmentsEnricher) { return new AuctionRequestFactory( maxRequestSize, ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, bidRequestOrtbVersionConversionManager, auctionGppService, cookieDeprecationService, @@ -427,7 +499,9 @@ AuctionRequestFactory auctionRequestFactory( ortbTypesResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper, + bidAdjustmentsEnricher); } @Bean @@ -450,6 +524,7 @@ IdGenerator sourceIdGenerator() { @Bean AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, StoredRequestProcessor storedRequestProcessor, + ProfilesProcessor profilesProcessor, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, AmpGppService ampGppService, OrtbTypesResolver ortbTypesResolver, @@ -458,11 +533,13 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, FpdResolver fpdResolver, AmpPrivacyContextFactory ampPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new AmpRequestFactory( ortb2RequestFactory, storedRequestProcessor, + profilesProcessor, bidRequestOrtbVersionConversionManager, ampGppService, ortbTypesResolver, @@ -471,7 +548,8 @@ AmpRequestFactory ampRequestFactory(Ortb2RequestFactory ortb2RequestFactory, fpdResolver, ampPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -485,7 +563,8 @@ VideoRequestFactory videoRequestFactory( Ortb2ImplicitParametersResolver ortb2ImplicitParametersResolver, AuctionPrivacyContextFactory auctionPrivacyContextFactory, DebugResolver debugResolver, - JacksonMapper mapper) { + JacksonMapper mapper, + GeoLocationServiceWrapper geoLocationServiceWrapper) { return new VideoRequestFactory( maxRequestSize, @@ -497,7 +576,8 @@ VideoRequestFactory videoRequestFactory( ortb2ImplicitParametersResolver, auctionPrivacyContextFactory, debugResolver, - mapper); + mapper, + geoLocationServiceWrapper); } @Bean @@ -508,7 +588,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 +602,7 @@ VideoStoredRequestProcessor videoStoredRequestProcessor( return new VideoStoredRequestProcessor( enforceStoredRequest, - splitToList(blacklistedAccountsString), + splitToList(blocklistedAccountsString), defaultTimeoutMs, adServerCurrency, defaultBidRequestPath, @@ -601,7 +681,7 @@ private static BasicHttpClient createBasicHttpClient(Vertx vertx, HttpClientProp .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) .setIdleTimeout(httpClientProperties.getIdleTimeoutMs()) .setPoolCleanerPeriod(httpClientProperties.getPoolCleanerPeriodMs()) - .setTryUseCompression(httpClientProperties.getUseCompression()) + .setDecompressionSupported(httpClientProperties.getUseCompression()) .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) // Vert.x's HttpClientRequest needs this value to be 2 for redirections to be followed once, // 3 for twice, and so on @@ -614,7 +694,7 @@ private static BasicHttpClient createBasicHttpClient(Vertx vertx, HttpClientProp options .setSsl(true) - .setKeyStoreOptions(jksOptions); + .setKeyCertOptions(jksOptions); } return new BasicHttpClient(vertx, vertx.createHttpClient(options)); @@ -637,6 +717,7 @@ UidsCookieService uidsCookieService( @Value("${host-cookie.domain:#{null}}") String hostCookieDomain, @Value("${host-cookie.ttl-days}") Integer ttlDays, @Value("${host-cookie.max-cookie-size-bytes}") Integer maxCookieSizeBytes, + @Value("${setuid.number-of-uid-cookies:1}") int numberOfUidCookies, PrioritizedCoopSyncProvider prioritizedCoopSyncProvider, Metrics metrics, JacksonMapper mapper) { @@ -649,6 +730,7 @@ UidsCookieService uidsCookieService( hostCookieDomain, ttlDays, maxCookieSizeBytes, + numberOfUidCookies, prioritizedCoopSyncProvider, metrics, mapper); @@ -697,9 +779,8 @@ CookieSyncService cookieSyncService( } @Bean - CookieDeprecationService deprecationCookieResolver(Account defaultAccount) { - - return new CookieDeprecationService(defaultAccount); + CookieDeprecationService cookieDeprecationService() { + return new CookieDeprecationService(); } @Bean @@ -712,20 +793,41 @@ BidderCatalog bidderCatalog(List bidderDeps) { return new BidderCatalog(bidderDeps); } + @Bean + BidderRequestCurrencyBlocker bidderRequestCurrencyBlocker(BidderCatalog bidderCatalog) { + return new BidderRequestCurrencyBlocker(bidderCatalog); + } + @Bean @ConditionalOnProperty(prefix = "auction.filter-imp-media-type", name = "enabled", havingValue = "true") - MediaTypeProcessor bidderMediaTypeProcessor(BidderCatalog bidderCatalog) { - return new BidderMediaTypeProcessor(bidderCatalog); + BidderRequestMediaFilter bidderRequestMediaFilter(BidderCatalog bidderCatalog) { + return new BidderRequestMediaFilter(bidderCatalog); } @Bean - MediaTypeProcessor multiFormatMediaTypeProcessor(BidderCatalog bidderCatalog) { - return new MultiFormatMediaTypeProcessor(bidderCatalog); + BidderRequestPreferredMediaProcessor bidderRequestPreferredMediaProcessor(BidderCatalog bidderCatalog) { + return new BidderRequestPreferredMediaProcessor(bidderCatalog); } @Bean - CompositeMediaTypeProcessor compositeMediaTypeProcessor(List mediaTypeProcessors) { - return new CompositeMediaTypeProcessor(mediaTypeProcessors); + BidderRequestCleaner bidderRequestCleaner() { + return new BidderRequestCleaner(); + } + + @Bean + CompositeBidderRequestPostProcessor compositeBidderRequestPostProcessor( + BidderRequestCurrencyBlocker bidderRequestCurrencyBlocker, + @Autowired(required = false) BidderRequestMediaFilter bidderRequestMediaFilter, + BidderRequestPreferredMediaProcessor bidderRequestPreferredMediaProcessor, + BidderRequestCleaner bidderRequestCleaner) { + + return new CompositeBidderRequestPostProcessor(Stream.of( + bidderRequestCurrencyBlocker, + bidderRequestMediaFilter, + bidderRequestPreferredMediaProcessor, + bidderRequestCleaner) + .filter(Objects::nonNull) + .toList()); } @Bean @@ -736,11 +838,13 @@ HttpBidderRequester httpBidderRequester( HttpBidderRequestEnricher requestEnricher, JacksonMapper mapper) { - return new HttpBidderRequester(httpClient, + return new HttpBidderRequester( + httpClient, bidderRequestCompletionTrackerFactory, bidderErrorNotifier, requestEnricher, - mapper); + mapper, + logSamplingRate); } @Bean @@ -773,9 +877,20 @@ BidderErrorNotifier bidderErrorNotifier( metrics); } + @Bean + CacheDefaultTtlProperties cacheDefaultTtlProperties( + @Value("${cache.default-ttl-seconds.banner:300}") Integer bannerTtl, + @Value("${cache.default-ttl-seconds.video:1500}") Integer videoTtl, + @Value("${cache.default-ttl-seconds.audio:1500}") Integer audioTtl, + @Value("${cache.default-ttl-seconds.native:300}") Integer nativeTtl) { + + return CacheDefaultTtlProperties.of(bannerTtl, videoTtl, audioTtl, nativeTtl); + } + @Bean BidResponseCreator bidResponseCreator( - CacheService cacheService, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, + CoreCacheService coreCacheService, BidderCatalog bidderCatalog, VastModifier vastModifier, EventsService eventsService, @@ -785,22 +900,33 @@ BidResponseCreator bidResponseCreator( HookStageExecutor hookStageExecutor, CategoryMappingService categoryMappingService, @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars, + @Value("${auction.enforce-random-bid-id:false}") boolean enforceRandomBidId, Clock clock, - JacksonMapper mapper) { + JacksonMapper mapper, + Metrics metrics, + @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl, + @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl, + CacheDefaultTtlProperties cacheDefaultTtlProperties) { return new BidResponseCreator( - cacheService, + logSamplingRate, + coreCacheService, bidderCatalog, vastModifier, eventsService, storedRequestProcessor, winningBidComparatorFactory, bidIdGenerator, + new UUIDIdGenerator(), hookStageExecutor, categoryMappingService, truncateAttrChars, + enforceRandomBidId, clock, - mapper); + mapper, + metrics, + CacheTtl.of(bannerCacheTtl, videoCacheTtl), + cacheDefaultTtlProperties); } @Bean @@ -808,28 +934,24 @@ 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, + CompositeBidderRequestPostProcessor bidderRequestPostProcessor, UidUpdater uidUpdater, TimeoutResolver timeoutResolver, 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,32 +962,38 @@ ExchangeService exchangeService( logSamplingRate, bidderCatalog, storedResponseProcessor, - dealsService, privacyEnforcementService, fpdResolver, + impAdjuster, supplyChainResolver, debugResolver, - mediaTypeProcessor, + bidderRequestPostProcessor, uidUpdater, timeoutResolver, timeoutFactory, bidRequestOrtbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyConversionService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, - applicationEventService, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, - dsaEnforcer, - bidAdjustmentFactorResolver, + priceFloorProcessor, + bidsAdjuster, metrics, clock, mapper, - criteriaLogManager, enabledStrictAppSiteDoohValidation); + criteriaLogManager, + enabledStrictAppSiteDoohValidation); + } + + @Bean + BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + BidAdjustmentsProcessor bidAdjustmentsProcessor) { + + return new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); } @Bean @@ -893,6 +1021,29 @@ StoredRequestProcessor storedRequestProcessor( jsonMerger); } + @Bean + ProfilesProcessor profilesProcessor(@Value("${auction.profiles.limit}") int maxProfiles, + @Value("${auction.profiles.timeout-ms}") long defaultTimeoutMillis, + @Value("${auction.profiles.fail-on-unknown:true}") boolean failOnUnknown, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate, + ApplicationSettings applicationSettings, + TimeoutFactory timeoutFactory, + Metrics metrics, + JacksonMapper mapper, + JsonMerger jsonMerger) { + + return new ProfilesProcessor( + maxProfiles, + defaultTimeoutMillis, + failOnUnknown, + logSamplingRate, + applicationSettings, + timeoutFactory, + metrics, + mapper, + jsonMerger); + } + @Bean WinningBidComparatorFactory winningBidComparatorFactory() { return new WinningBidComparatorFactory(); @@ -906,16 +1057,8 @@ StoredResponseProcessor storedResponseProcessor(ApplicationSettings applicationS } @Bean - PrivacyEnforcementService privacyEnforcementService(CoppaEnforcement coppaEnforcement, - CcpaEnforcement ccpaEnforcement, - TcfEnforcement tcfEnforcement, - ActivityEnforcement activityEnforcement) { - - return new PrivacyEnforcementService( - coppaEnforcement, - ccpaEnforcement, - tcfEnforcement, - activityEnforcement); + PrivacyEnforcementService privacyEnforcementService(List enforcements) { + return new PrivacyEnforcementService(enforcements); } @Bean @@ -982,27 +1125,44 @@ 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, - @Value("${auction.strict-app-site-dooh:false}") boolean enabledStrictAppSiteDoohValidation) { + @Value("${auction.strict-app-site-dooh:false}") boolean enabledStrictAppSiteDoohValidation, + @Value("${settings.fail-on-disabled-bidders:true}") boolean failOnDisabledBidders, + @Value("${settings.fail-on-unknown-bidders:true}") boolean failOnUnknownBidders) { return new RequestValidator( bidderCatalog, - bidderParamValidator, + impValidator, metrics, mapper, logSamplingRate, - enabledStrictAppSiteDoohValidation); + enabledStrictAppSiteDoohValidation, + failOnDisabledBidders, + failOnUnknownBidders); } @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 +1174,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 +1280,43 @@ 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) { + return new SkippedAuctionService(storedResponseProcessor); + } + + @Bean + BidAdjustmentsEnricher bidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger) { + return new BidAdjustmentsEnricher(mapper, jsonMerger, logSamplingRate); + } + + @Bean + BidAdjustmentsResolver bidAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new BidAdjustmentsResolver(currencyService, bidAdjustmentsRulesResolver); + } + + @Bean + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver(JacksonMapper mapper) { + return new BidAdjustmentsRulesResolver(mapper); + } + + @Bean + BidAdjustmentsProcessor bidAdjustmentsProcessor(CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidAdjustmentsResolver bidAdjustmentsResolver, + JacksonMapper mapper) { + + return new BidAdjustmentsProcessor( + currencyService, + bidAdjustmentFactorResolver, + bidAdjustmentsResolver, + mapper); } 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..79517afcc6d 100644 --- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java @@ -6,26 +6,33 @@ import lombok.NoArgsConstructor; import lombok.experimental.UtilityClass; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.execution.TimeoutFactory; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.activity.ActivitiesConfigResolver; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.PriceFloorsConfigResolver; 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.MetricName; import org.prebid.server.metric.Metrics; 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.model.Profile; +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,17 +43,30 @@ 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.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkClientException; +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 public class SettingsConfiguration { + private static final Logger logger = LoggerFactory.getLogger(SettingsConfiguration.class); + @Configuration @ConditionalOnProperty(prefix = "settings.filesystem", name = {"settings-filename", "stored-requests-dir", "stored-imps-dir"}) @@ -57,13 +77,21 @@ FileApplicationSettings fileApplicationSettings( @Value("${settings.filesystem.settings-filename}") String settingsFileName, @Value("${settings.filesystem.stored-requests-dir}") String storedRequestsDir, @Value("${settings.filesystem.stored-imps-dir}") String storedImpsDir, + @Value("${settings.filesystem.profiles-dir:#{null}}") String profilesDir, @Value("${settings.filesystem.stored-responses-dir}") String storedResponsesDir, @Value("${settings.filesystem.categories-dir}") String categoriesDir, FileSystem fileSystem, JacksonMapper jacksonMapper) { - return new FileApplicationSettings(fileSystem, settingsFileName, storedRequestsDir, storedImpsDir, - storedResponsesDir, categoriesDir, jacksonMapper); + return new FileApplicationSettings( + fileSystem, + settingsFileName, + storedRequestsDir, + storedImpsDir, + profilesDir, + storedResponsesDir, + categoriesDir, + jacksonMapper); } } @@ -72,20 +100,24 @@ 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.profiles-query:#{null}}") String profilesQuery, @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, + profilesQuery, storedResponsesQuery); } } @@ -96,6 +128,7 @@ static class HttpSettingsConfiguration { @Bean HttpApplicationSettings httpApplicationSettings( + @Value("${settings.http.rfc3986-compatible:false}") boolean isRfc3986Compatible, HttpClient httpClient, JacksonMapper mapper, @Value("${settings.http.endpoint}") String endpoint, @@ -103,8 +136,14 @@ HttpApplicationSettings httpApplicationSettings( @Value("${settings.http.video-endpoint}") String videoEndpoint, @Value("${settings.http.category-endpoint}") String categoryEndpoint) { - return new HttpApplicationSettings(httpClient, mapper, endpoint, ampEndpoint, videoEndpoint, - categoryEndpoint); + return new HttpApplicationSettings( + isRfc3986Compatible, + endpoint, + ampEndpoint, + videoEndpoint, + categoryEndpoint, + httpClient, + mapper); } } @@ -128,7 +167,7 @@ static class HttpPeriodicRefreshServiceConfiguration { @Bean public HttpPeriodicRefreshService httpPeriodicRefreshService( @Value("${settings.in-memory-cache.http-update.endpoint}") String endpoint, - SettingsCache settingsCache, + SettingsCache settingsCache, JacksonMapper mapper) { return new HttpPeriodicRefreshService( @@ -138,7 +177,7 @@ public HttpPeriodicRefreshService httpPeriodicRefreshService( @Bean public HttpPeriodicRefreshService ampHttpPeriodicRefreshService( @Value("${settings.in-memory-cache.http-update.amp-endpoint}") String ampEndpoint, - SettingsCache ampSettingsCache, + SettingsCache ampSettingsCache, JacksonMapper mapper) { return new HttpPeriodicRefreshService( @@ -148,21 +187,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 +213,12 @@ static class JdbcPeriodicRefreshServiceConfiguration { Clock clock; @Bean - public JdbcPeriodicRefreshService jdbcPeriodicRefreshService( - @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) { + public DatabasePeriodicRefreshService databasePeriodicRefreshService( + @Qualifier("settingsCache") SettingsCache settingsCache, + @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 +226,19 @@ public JdbcPeriodicRefreshService jdbcPeriodicRefreshService( MetricName.stored_request, settingsCache, vertx, - jdbcClient, + databaseClient, timeoutFactory, metrics, clock); } @Bean - public JdbcPeriodicRefreshService ampJdbcPeriodicRefreshService( - @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) { + public DatabasePeriodicRefreshService ampDatabasePeriodicRefreshService( + @Qualifier("ampSettingsCache") SettingsCache ampSettingsCache, + @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 +246,145 @@ 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") + @Validated + @Data + @NoArgsConstructor + protected static class S3ConfigurationProperties { + + /** + * If accessKeyId and secretAccessKey are provided in the + * configuration file then they will be used. Otherwise, the + * DefaultCredentialsProvider will look for credentials in this order: + *

+ * - Java System Properties + * - Environment Variables + * - Web Identity Token + * - AWS credentials file (~/.aws/credentials) + * - ECS container credentials + * - EC2 instance profile + */ + private String accessKeyId; + private String secretAccessKey; + + private boolean useStaticCredentials() { + return StringUtils.isNotBlank(accessKeyId) && StringUtils.isNotBlank(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 Region awsRegion = Optional.ofNullable(s3ConfigurationProperties.getRegion()) + .map(Region::of) + .orElse(Region.AWS_GLOBAL); + + return S3AsyncClient.builder() + .credentialsProvider(awsCredentialsProvider(s3ConfigurationProperties)) + .endpointOverride(new URI(s3ConfigurationProperties.getEndpoint())) + .forcePathStyle(s3ConfigurationProperties.getForcePathStyle()) + .region(awsRegion) + .build(); + } + + private static AwsCredentialsProvider awsCredentialsProvider(S3ConfigurationProperties config) { + final AwsCredentialsProvider credentialsProvider = config.useStaticCredentials() + ? StaticCredentialsProvider.create( + AwsBasicCredentials.create(config.getAccessKeyId(), config.getSecretAccessKey())) + : DefaultCredentialsProvider.create(); + + try { + credentialsProvider.resolveCredentials(); + } catch (SdkClientException e) { + logger.error("Failed to resolve AWS credentials", e); + } + + return credentialsProvider; + } + + @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 +394,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 +416,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); } } @@ -267,9 +442,10 @@ static class CachingSettingsConfiguration { CachingApplicationSettings cachingApplicationSettings( EnrichingApplicationSettings enrichingApplicationSettings, ApplicationSettingsCacheProperties cacheProperties, - @Qualifier("settingsCache") SettingsCache cache, - @Qualifier("ampSettingsCache") SettingsCache ampCache, - @Qualifier("videoSettingCache") SettingsCache videoCache, + @Qualifier("settingsCache") SettingsCache cache, + @Qualifier("ampSettingsCache") SettingsCache ampCache, + @Qualifier("videoSettingCache") SettingsCache videoCache, + @Qualifier("profileSettingCache") SettingsCache profilesCache, Metrics metrics) { return new CachingApplicationSettings( @@ -277,9 +453,11 @@ CachingApplicationSettings cachingApplicationSettings( cache, ampCache, videoCache, + profilesCache, metrics, cacheProperties.getTtlSeconds(), - cacheProperties.getCacheSize()); + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } } @@ -300,20 +478,38 @@ static class CacheConfiguration { @Bean @Qualifier("settingsCache") - SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + SettingsCache settingsCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } @Bean @Qualifier("ampSettingsCache") - SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + SettingsCache ampSettingsCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } @Bean @Qualifier("videoSettingCache") - SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperties) { - return new SettingsCache(cacheProperties.getTtlSeconds(), cacheProperties.getCacheSize()); + SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); + } + + @Bean + @Qualifier("profileSettingCache") + SettingsCache profileSettingCache(ApplicationSettingsCacheProperties cacheProperties) { + return new SettingsCache<>( + cacheProperties.getTtlSeconds(), + cacheProperties.getCacheSize(), + cacheProperties.getJitterSeconds()); } } @@ -323,7 +519,7 @@ SettingsCache videoSettingCache(ApplicationSettingsCacheProperties cacheProperti @Validated @Data @NoArgsConstructor - private static class ApplicationSettingsCacheProperties { + protected static class ApplicationSettingsCacheProperties { @NotNull @Min(1) @@ -331,5 +527,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..3ac62c8fd59 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,16 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; -import io.vertx.core.eventbus.EventBus; +import io.vertx.core.dns.AddressResolverOptions; 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; @@ -25,7 +24,9 @@ public class VertxConfiguration { @Bean Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize, @Value("${vertx.enable-per-client-endpoint-metrics}") boolean enablePerClientEndpointMetrics, - @Value("${metrics.jmx.enabled}") boolean jmxEnabled) { + @Value("${metrics.jmx.enabled}") boolean jmxEnabled, + @Value("${vertx.round-robin-inet-address}") boolean roundRobinInetAddress) { + final DropwizardMetricsOptions metricsOptions = new DropwizardMetricsOptions() .setEnabled(true) .setJmxEnabled(jmxEnabled) @@ -34,24 +35,20 @@ Vertx vertx(@Value("${vertx.worker-pool-size}") int workerPoolSize, metricsOptions.addMonitoredHttpClientEndpoint(new Match().setValue(".*").setType(MatchType.REGEX)); } + final AddressResolverOptions addressResolverOptions = new AddressResolverOptions(); + addressResolverOptions.setRoundRobinInetAddress(roundRobinInetAddress); + final VertxOptions vertxOptions = new VertxOptions() .setPreferNativeTransport(true) .setWorkerPoolSize(workerPoolSize) - .setMetricsOptions(metricsOptions); + .setMetricsOptions(metricsOptions) + .setAddressResolverOptions(addressResolverOptions); 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/AdagioConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java new file mode 100644 index 00000000000..cd4544370a8 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdagioConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adagio.AdagioBidder; +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/adagio.yaml", factory = YamlPropertySourceFactory.class) +public class AdagioConfiguration { + + private static final String BIDDER_NAME = "adagio"; + + @Bean("adagioConfigurationProperties") + @ConfigurationProperties("adapters.adagio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adagioBidderDeps(BidderConfigurationProperties adagioConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adagioConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdagioBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..3aca2e59cf8 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 @@ -1,5 +1,8 @@ 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.adnuntius.AdnuntiusBidder; import org.prebid.server.json.JacksonMapper; @@ -13,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.time.Clock; @Configuration @@ -24,20 +27,32 @@ public class AdnuntiusBidderConfiguration { @Bean("adnuntiusConfigurationProperties") @ConfigurationProperties("adapters.adnuntius") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + AdnuntiusConfigurationProperties configurationProperties() { + return new AdnuntiusConfigurationProperties(); } @Bean - BidderDeps adnuntiusBidderDeps(BidderConfigurationProperties adnuntiusConfigurationProperties, + BidderDeps adnuntiusBidderDeps(AdnuntiusConfigurationProperties adnuntiusConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, Clock clock, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) + return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(adnuntiusConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AdnuntiusBidder(config.getEndpoint(), clock, mapper)) + .bidderCreator(config -> new AdnuntiusBidder( + config.getEndpoint(), + config.getEuEndpoint(), + clock, + mapper)) .assemble(); } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class AdnuntiusConfigurationProperties extends BidderConfigurationProperties { + + private String euEndpoint; + } } 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 deleted file mode 100644 index aabbf780e6a..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdopplerConfiguration.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.adoppler.AdopplerBidder; -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/adoppler.yaml", factory = YamlPropertySourceFactory.class) -public class AdopplerConfiguration { - - private static final String BIDDER_NAME = "adoppler"; - - @Bean("adopplerConfigurationProperties") - @ConfigurationProperties("adapters.adoppler") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps adopplerBidderDeps(BidderConfigurationProperties adopplerConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(adopplerConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AdopplerBidder(config.getEndpoint(), mapper)) - .assemble(); - } -} 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/AduptechConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java new file mode 100644 index 00000000000..5011fc46c41 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AduptechConfiguration.java @@ -0,0 +1,60 @@ +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.aduptech.AduptechBidder; +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; +import jakarta.validation.constraints.NotNull; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/aduptech.yaml", factory = YamlPropertySourceFactory.class) +public class AduptechConfiguration { + + private static final String BIDDER_NAME = "aduptech"; + + @Bean("aduptechConfigurationProperties") + @ConfigurationProperties("adapters.aduptech") + AduptechConfigurationProperties configurationProperties() { + return new AduptechConfigurationProperties(); + } + + @Bean + BidderDeps aduptechBidderDeps(AduptechConfigurationProperties aduptechConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(aduptechConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AduptechBidder( + config.getEndpoint(), + mapper, + currencyConversionService, + config.getTargetCurrency())) + .assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class AduptechConfigurationProperties extends BidderConfigurationProperties { + + @NotNull + private String targetCurrency; + } +} 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..79d50338b1d 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) @@ -39,4 +39,3 @@ BidderDeps advangelistsBidderDeps(BidderConfigurationProperties advangelistsConf .assemble(); } } - diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdverxoBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdverxoBidderConfiguration.java new file mode 100644 index 00000000000..ae9224670b7 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdverxoBidderConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.adverxo.AdverxoBidder; +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/adverxo.yaml", factory = YamlPropertySourceFactory.class) +public class AdverxoBidderConfiguration { + + private static final String BIDDER_NAME = "adverxo"; + + @Bean("adverxoConfigurationProperties") + @ConfigurationProperties("adapters.adverxo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps adverxoBidderDeps(BidderConfigurationProperties adverxoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(adverxoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AdverxoBidder(config.getEndpoint(), mapper, currencyConversionService)) + .assemble(); + } +} 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/AfrontConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AfrontConfiguration.java new file mode 100644 index 00000000000..60617aa5ea8 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AfrontConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.afront.AfrontBidder; +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/afront.yaml", factory = YamlPropertySourceFactory.class) +public class AfrontConfiguration { + + private static final String BIDDER_NAME = "afront"; + + @Bean("afrontConfigurationProperties") + @ConfigurationProperties("adapters.afront") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps afrontBidderDeps(BidderConfigurationProperties afrontConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(afrontConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AfrontBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/AkceloConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AkceloConfiguration.java new file mode 100644 index 00000000000..3b6472eac49 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/AkceloConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.akcelo.AkceloBidder; +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/akcelo.yaml", + factory = YamlPropertySourceFactory.class) +public class AkceloConfiguration { + + private static final String BIDDER_NAME = "akcelo"; + + @Bean("akceloConfigurationProperties") + @ConfigurationProperties("adapters.akcelo") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps akceloBidderDeps(BidderConfigurationProperties akceloConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(akceloConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new AkceloBidder(config.getEndpoint(), mapper)) + .assemble(); + } + +} 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..b75d013a8d6 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) @@ -39,4 +39,3 @@ BidderDeps axisBidderDeps(BidderConfigurationProperties axisConfigurationPropert .assemble(); } } - 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..39bdebda807 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) @@ -39,4 +39,3 @@ BidderDeps axonixBidderDeps(BidderConfigurationProperties axonixConfigurationPro .assemble(); } } - 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..e5ec3bc1557 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) @@ -39,4 +39,3 @@ BidderDeps bematterfullBidderDeps(BidderConfigurationProperties bematterfullConf .assemble(); } } - 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/BidTheatreConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java new file mode 100644 index 00000000000..2cbf98062df --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BidTheatreConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.bidtheatre.BidTheatreBidder; +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/bidtheatre.yaml", factory = YamlPropertySourceFactory.class) +public class BidTheatreConfiguration { + + private static final String BIDDER_NAME = "bidtheatre"; + + @Bean("bidtheatreConfigurationProperties") + @ConfigurationProperties("adapters.bidtheatre") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps bidtheatreBidderDeps(BidderConfigurationProperties bidtheatreConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(bidtheatreConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BidTheatreBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/BlisBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BlisBidderConfiguration.java new file mode 100644 index 00000000000..47d2f796f40 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BlisBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.blis.BlisBidder; +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/blis.yaml", factory = YamlPropertySourceFactory.class) +public class BlisBidderConfiguration { + + private static final String BIDDER_NAME = "blis"; + + @Bean("blisConfigurationProperties") + @ConfigurationProperties("adapters.blis") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps blisBidderDeps(BidderConfigurationProperties blisConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(blisConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BlisBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/BoldwinRapidConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/BoldwinRapidConfiguration.java new file mode 100644 index 00000000000..be498585d72 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/BoldwinRapidConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.boldwinrapid.BoldwinRapidBidder; +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/boldwinrapid.yaml", factory = YamlPropertySourceFactory.class) +public class BoldwinRapidConfiguration { + + private static final String BIDDER_NAME = "boldwin_rapid"; + + @Bean("boldwinRapidConfigurationProperties") + @ConfigurationProperties("adapters.boldwinrapid") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps boldwinrapidBidderDeps(BidderConfigurationProperties boldwinRapidConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(boldwinRapidConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new BoldwinRapidBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/ConnatixConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ConnatixConfiguration.java new file mode 100644 index 00000000000..09754adc489 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ConnatixConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.connatix.ConnatixBidder; +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/connatix.yaml", factory = YamlPropertySourceFactory.class) +public class ConnatixConfiguration { + + private static final String BIDDER_NAME = "connatix"; + + @Bean("connatixConfigurationProperties") + @ConfigurationProperties("adapters.connatix") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps connatixBidderDeps(BidderConfigurationProperties connatixConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(connatixConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ConnatixBidder(config.getEndpoint(), currencyConversionService, 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/ContxtfulConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ContxtfulConfiguration.java new file mode 100644 index 00000000000..0759f38bc17 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ContxtfulConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.contxtful.ContxtfulBidder; +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/contxtful.yaml", factory = YamlPropertySourceFactory.class) +public class ContxtfulConfiguration { + + private static final String BIDDER_NAME = "contxtful"; + + @Bean("contxtfulConfigurationProperties") + @ConfigurationProperties("adapters.contxtful") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps contxtfulBidderDeps(BidderConfigurationProperties contxtfulConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(contxtfulConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ContxtfulBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/ElementalTVConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ElementalTVConfiguration.java new file mode 100644 index 00000000000..8cece283dd5 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ElementalTVConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.elementaltv.ElementalTVBidder; +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/elementaltv.yaml", factory = YamlPropertySourceFactory.class) +public class ElementalTVConfiguration { + + private static final String BIDDER_NAME = "elementaltv"; + + @Bean("elementalTVConfigurationProperties") + @ConfigurationProperties("adapters.elementaltv") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps elementaltvBidderDeps(BidderConfigurationProperties elementalTVConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(elementalTVConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ElementalTVBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..dbeccd07032 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) @@ -39,4 +39,3 @@ BidderDeps emxdigitalBidderDeps(BidderConfigurationProperties emxdigitalConfigur .assemble(); } } - 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/ExcoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ExcoConfiguration.java new file mode 100644 index 00000000000..9e079821d2e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ExcoConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.exco.ExcoBidder; +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/exco.yaml", factory = YamlPropertySourceFactory.class) +public class ExcoConfiguration { + + private static final String BIDDER_NAME = "exco"; + + @Bean("excoConfigurationProperties") + @ConfigurationProperties("adapters.exco") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps excoBidderDeps(BidderConfigurationProperties excoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(excoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ExcoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FeedAdConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FeedAdConfiguration.java new file mode 100644 index 00000000000..a716452fbfa --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/FeedAdConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.feedad.FeedAdBidder; +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/feedad.yaml", factory = YamlPropertySourceFactory.class) +public class FeedAdConfiguration { + + private static final String BIDDER_NAME = "feedad"; + + @Bean("feedadConfigurationProperties") + @ConfigurationProperties("adapters.feedad") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps feedadBidderDeps(BidderConfigurationProperties feedadConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(feedadConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new FeedAdBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java new file mode 100644 index 00000000000..eef57c1c0a9 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/FlatadsConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.flatads.FlatadsBidder; +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/flatads.yaml", factory = YamlPropertySourceFactory.class) +public class FlatadsConfiguration { + + private static final String BIDDER_NAME = "flatads"; + + @Bean("flatadsConfigurationProperties") + @ConfigurationProperties("adapters.flatads") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps flatadsBidderDeps(BidderConfigurationProperties flatadsConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(flatadsConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new FlatadsBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..b26cefb1909 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) @@ -39,4 +39,3 @@ BidderDeps gammaBidderDeps(BidderConfigurationProperties gammaConfigurationPrope .assemble(); } } - 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/InsticatorConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InsticatorConfiguration.java new file mode 100644 index 00000000000..7ae0e1a3432 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/InsticatorConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.insticator.InsticatorBidder; +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/insticator.yaml", factory = YamlPropertySourceFactory.class) +public class InsticatorConfiguration { + + private static final String BIDDER_NAME = "insticator"; + + @Bean("insticatorConfigurationProperties") + @ConfigurationProperties("adapters.insticator") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps insticatorBidderDeps(BidderConfigurationProperties insticatorConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(insticatorConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new InsticatorBidder(currencyConversionService, config.getEndpoint(), mapper)) + .assemble(); + } +} 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/KoblerConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KoblerConfiguration.java new file mode 100644 index 00000000000..eaa41c79bde --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/KoblerConfiguration.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.kobler.KoblerBidder; +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 org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; + +@Configuration +@PropertySource(value = "classpath:/bidder-config/kobler.yaml", factory = YamlPropertySourceFactory.class) +public class KoblerConfiguration { + + private static final String BIDDER_NAME = "kobler"; + + @Bean("koblerConfigurationProperties") + @ConfigurationProperties("adapters.kobler") + KoblerConfigurationProperties configurationProperties() { + return new KoblerConfigurationProperties(); + } + + @Bean + BidderDeps koblerBidderDeps(KoblerConfigurationProperties config, + CurrencyConversionService currencyConversionService, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(config) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(cfg -> new KoblerBidder( + cfg.getEndpoint(), + cfg.getDevEndpoint(), + currencyConversionService, + mapper)) + .assemble(); + + } + + @Validated + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class KoblerConfigurationProperties extends BidderConfigurationProperties { + + @NotBlank + private String devEndpoint; + } +} 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/KueezRtbConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java new file mode 100644 index 00000000000..06e89467d0c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/KueezRtbConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.kueezrtb.KueezRtbBidder; +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/kueezrtb.yaml", factory = YamlPropertySourceFactory.class) +public class KueezRtbConfiguration { + + private static final String BIDDER_NAME = "kueezrtb"; + + @Bean("kueezrtbConfigurationProperties") + @ConfigurationProperties("adapters.kueezrtb") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps kueezrtbBidderDeps(BidderConfigurationProperties kueezrtbConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(kueezrtbConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new KueezRtbBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/MadsenseConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java new file mode 100644 index 00000000000..a1cc34057e3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MadsenseConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.madsense.MadsenseBidder; +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/madsense.yaml", factory = YamlPropertySourceFactory.class) +public class MadsenseConfiguration { + + private static final String BIDDER_NAME = "madsense"; + + @Bean("madsenseConfigurationProperties") + @ConfigurationProperties("adapters.madsense") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps madsenseBidderDeps(BidderConfigurationProperties madsenseConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(madsenseConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MadsenseBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/MediasquareConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MediasquareConfiguration.java new file mode 100644 index 00000000000..bbdd5b6ed2b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MediasquareConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mediasquare.MediasquareBidder; +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/mediasquare.yaml", factory = YamlPropertySourceFactory.class) +public class MediasquareConfiguration { + + private static final String BIDDER_NAME = "mediasquare"; + + @Bean("mediasquareConfigurationProperties") + @ConfigurationProperties("adapters.mediasquare") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mediasquareBidderDeps(BidderConfigurationProperties mediasquareConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(mediasquareConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MediasquareBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..fd32784ea58 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 @@ -1,5 +1,8 @@ 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.minutemedia.MinuteMediaBidder; import org.prebid.server.json.JacksonMapper; @@ -13,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; @Configuration @PropertySource(value = "classpath:/bidder-config/minutemedia.yaml", factory = YamlPropertySourceFactory.class) @@ -23,19 +26,30 @@ public class MinuteMediaConfiguration { @Bean("minutemediaConfigurationProperties") @ConfigurationProperties("adapters.minutemedia") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + MinuteMediaConfigurationProperties configurationProperties() { + return new MinuteMediaConfigurationProperties(); } @Bean - BidderDeps minutemediaBidderDeps(BidderConfigurationProperties minutemediaConfigurationProperties, + BidderDeps minutemediaBidderDeps(MinuteMediaConfigurationProperties minutemediaConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) + return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(minutemediaConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new MinuteMediaBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new MinuteMediaBidder( + config.getEndpoint(), + config.getTestEndpoint(), + mapper)) .assemble(); } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class MinuteMediaConfigurationProperties extends BidderConfigurationProperties { + + private String testEndpoint; + } } 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..798f57dc35e --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MissenaConfiguration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.missena.MissenaBidder; +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.prebid.server.version.PrebidVersionProvider; +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, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(missenaConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MissenaBidder( + config.getEndpoint(), mapper, currencyConversionService, prebidVersionProvider)) + .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/MobkoiConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java new file mode 100644 index 00000000000..9b4761fdfaf --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/MobkoiConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.mobkoi.MobkoiBidder; +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/mobkoi.yaml", factory = YamlPropertySourceFactory.class) +public class MobkoiConfiguration { + + private static final String BIDDER_NAME = "mobkoi"; + + @Bean("mobkoiConfigurationProperties") + @ConfigurationProperties("adapters.mobkoi") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps mobkoiBidderDeps(BidderConfigurationProperties mobkoiConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(mobkoiConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new MobkoiBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/NativeryBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/NativeryBidderConfiguration.java new file mode 100644 index 00000000000..0dc07f48e94 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/NativeryBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.nativery.NativeryBidder; +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/nativery.yaml", factory = YamlPropertySourceFactory.class) +public class NativeryBidderConfiguration { + + private static final String BIDDER_NAME = "nativery"; + + @Bean("nativeryConfigurationProperties") + @ConfigurationProperties("adapters.nativery") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(nativeryConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new NativeryBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..4d102b90f9c 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 @@ -10,13 +10,14 @@ 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; 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 @@ -34,6 +35,7 @@ NextMillenniumConfigurationProperties configurationProperties() { @Bean BidderDeps nextMillenniumBidderDeps(NextMillenniumConfigurationProperties nextMillenniumConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, + PrebidVersionProvider prebidVersionProvider, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) @@ -42,7 +44,8 @@ BidderDeps nextMillenniumBidderDeps(NextMillenniumConfigurationProperties nextMi .bidderCreator(config -> new NextMillenniumBidder( config.getEndpoint(), mapper, - config.getExtraInfo().getNmmFlags()) + config.getExtraInfo().getNmmFlags(), + prebidVersionProvider) ).assemble(); } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java new file mode 100644 index 00000000000..2eccf05c68c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/Nexx360Configuration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.nexx360.Nexx360Bidder; +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.prebid.server.version.PrebidVersionProvider; +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/nexx360.yaml", factory = YamlPropertySourceFactory.class) +public class Nexx360Configuration { + + private static final String BIDDER_NAME = "nexx360"; + + @Bean("nexx360ConfigurationProperties") + @ConfigurationProperties("adapters.nexx360") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps nexx360BidderDeps(BidderConfigurationProperties nexx360ConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(nexx360ConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new Nexx360Bidder( + config.getEndpoint(), + mapper, + prebidVersionProvider)) + .assemble(); + } +} 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/OguryConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java new file mode 100644 index 00000000000..5e6174a3e35 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OguryConfiguration.java @@ -0,0 +1,43 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.ogury.OguryBidder; +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/ogury.yaml", factory = YamlPropertySourceFactory.class) +public class OguryConfiguration { + + private static final String BIDDER_NAME = "ogury"; + + @Bean("oguryConfigurationProperties") + @ConfigurationProperties("adapters.ogury") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps oguryBidderDeps(BidderConfigurationProperties oguryConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(oguryConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OguryBidder(config.getEndpoint(), currencyConversionService, mapper)) + .assemble(); + } +} 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..92062b99e1e 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) @@ -39,4 +39,3 @@ BidderDeps omsBidderDeps(BidderConfigurationProperties omsConfigurationPropertie .assemble(); } } - 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/OptidigitalConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/OptidigitalConfiguration.java new file mode 100644 index 00000000000..dbd9309821b --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/OptidigitalConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.optidigital.OptidigitalBidder; +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/optidigital.yaml", factory = YamlPropertySourceFactory.class) +public class OptidigitalConfiguration { + + private static final String BIDDER_NAME = "optidigital"; + + @Bean("optidigitalConfigurationProperties") + @ConfigurationProperties("adapters.optidigital") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps optidigitalBidderDeps(BidderConfigurationProperties optidigitalConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(optidigitalConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new OptidigitalBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..7296f81625b 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 @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.pgamssp.PgamSspBidder; +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/pgamssp.yaml", factory = YamlPropertySourceFactory.class) @@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps pgamsspBidderDeps(BidderConfigurationProperties pgamsspConfigurationProperties, + CurrencyConversionService currencyConversionService, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(pgamsspConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } 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/RediadsConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/RediadsConfiguration.java new file mode 100644 index 00000000000..8339e77c199 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/RediadsConfiguration.java @@ -0,0 +1,56 @@ +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.rediads.RediadsBidder; +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/rediads.yaml", factory = YamlPropertySourceFactory.class) +public class RediadsConfiguration { + + private static final String BIDDER_NAME = "rediads"; + + @Bean("rediadsConfigurationProperties") + @ConfigurationProperties("adapters.rediads") + RediadsConfigurationProperties configurationProperties() { + return new RediadsConfigurationProperties(); + } + + @Bean + BidderDeps rediadsBidderDeps(RediadsConfigurationProperties rediadsConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(rediadsConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new RediadsBidder( + config.getEndpoint(), + mapper, + config.getDefaultSubdomain())) + .assemble(); + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class RediadsConfigurationProperties extends BidderConfigurationProperties { + + @NotBlank + private String defaultSubdomain; + } +} 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..7089abb6d78 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 @@ -7,11 +7,13 @@ import org.prebid.server.bidder.rubicon.RubiconBidder; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.floors.PriceFloorResolver; +import org.prebid.server.identity.UUIDIdGenerator; 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.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 +21,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 +42,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 +50,18 @@ 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(), + config.getGenerateBidId() == null || config.getGenerateBidId(), + config.getApexRendererUrl(), currencyConversionService, floorResolver, + versionProvider, + new UUIDIdGenerator(), mapper)) .assemble(); } @@ -68,8 +76,10 @@ private static class RubiconConfigurationProperties extends BidderConfigurationP @NotNull private XAPI xapi = new XAPI(); - @NotNull private Boolean generateBidId; + + @NotNull + private String apexRendererUrl; } @Data 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/SeedtagConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java new file mode 100644 index 00000000000..08d5c527d62 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SeedtagConfiguration.java @@ -0,0 +1,46 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.seedtag.SeedtagBidder; +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/seedtag.yaml", factory = YamlPropertySourceFactory.class) +public class SeedtagConfiguration { + + private static final String BIDDER_NAME = "seedtag"; + + @Bean("seedtagConfigurationProperties") + @ConfigurationProperties("adapters.seedtag") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps seedtagBidderDeps(BidderConfigurationProperties seedtagConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(seedtagConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SeedtagBidder( + config.getEndpoint(), + currencyConversionService, + mapper)) + .assemble(); + } +} 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/ShowheroesConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ShowheroesConfiguration.java new file mode 100644 index 00000000000..260d8376675 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ShowheroesConfiguration.java @@ -0,0 +1,49 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.showheroes.ShowheroesBidder; +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.prebid.server.version.PrebidVersionProvider; +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/showheroes.yaml", factory = YamlPropertySourceFactory.class) +public class ShowheroesConfiguration { + + private static final String BIDDER_NAME = "showheroes"; + + @Bean("showheroesConfigurationProperties") + @ConfigurationProperties("adapters.showheroes") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps showheroesBidderDeps(BidderConfigurationProperties showheroesConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + CurrencyConversionService currencyConversionService, + PrebidVersionProvider prebidVersionProvider, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(showheroesConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ShowheroesBidder( + config.getEndpoint(), + currencyConversionService, + prebidVersionProvider, + mapper)) + .assemble(); + } +} 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 deleted file mode 100644 index 28c50df1619..00000000000 --- a/src/main/java/org/prebid/server/spring/config/bidder/SimpleWantedConfiguration.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.smilewanted.SmileWantedBidder; -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/smilewanted.yaml", factory = YamlPropertySourceFactory.class) -public class SimpleWantedConfiguration { - - private static final String BIDDER_NAME = "smilewanted"; - - @Bean("smilewantedConfigurationProperties") - @ConfigurationProperties("adapters.smilewanted") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); - } - - @Bean - BidderDeps smilewantedBidderDeps(BidderConfigurationProperties smilewantedConfigurationProperties, - @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { - - return BidderDepsAssembler.forBidder(BIDDER_NAME) - .withConfig(smilewantedConfigurationProperties) - .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SmileWantedBidder(config.getEndpoint(), mapper)) - .assemble(); - } -} 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..3d831685aff 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 @@ -1,5 +1,8 @@ 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.smartadserver.SmartadserverBidder; import org.prebid.server.json.JacksonMapper; @@ -13,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; @Configuration @PropertySource(value = "classpath:/bidder-config/smartadserver.yaml", factory = YamlPropertySourceFactory.class) @@ -23,19 +26,28 @@ public class SmartadserverConfiguration { @Bean("smartadserverConfigurationProperties") @ConfigurationProperties("adapters.smartadserver") - BidderConfigurationProperties configurationProperties() { - return new BidderConfigurationProperties(); + SmartadserverConfigurationProperties configurationProperties() { + return new SmartadserverConfigurationProperties(); } @Bean - BidderDeps smartadserverBidderDeps(BidderConfigurationProperties smartadserverConfigurationProperties, + BidderDeps smartadserverBidderDeps(SmartadserverConfigurationProperties smartadserverConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, JacksonMapper mapper) { - return BidderDepsAssembler.forBidder(BIDDER_NAME) + return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(smartadserverConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new SmartadserverBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new SmartadserverBidder( + config.getEndpoint(), config.getSecondaryEndpoint(), mapper)) .assemble(); } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + private static class SmartadserverConfigurationProperties extends BidderConfigurationProperties { + + private String secondaryEndpoint; + } } 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/SmileWantedConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmileWantedConfiguration.java new file mode 100644 index 00000000000..eb3ae5bb83c --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmileWantedConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.smilewanted.SmileWantedBidder; +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/smilewanted.yaml", factory = YamlPropertySourceFactory.class) +public class SmileWantedConfiguration { + + private static final String BIDDER_NAME = "smilewanted"; + + @Bean("smilewantedConfigurationProperties") + @ConfigurationProperties("adapters.smilewanted") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps smilewantedBidderDeps(BidderConfigurationProperties smilewantedConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(smilewantedConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SmileWantedBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java new file mode 100644 index 00000000000..154bb8c4779 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SmootConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.smoot.SmootBidder; +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/smoot.yaml", factory = YamlPropertySourceFactory.class) +public class SmootConfiguration { + + private static final String BIDDER_NAME = "smoot"; + + @Bean("smootConfigurationProperties") + @ConfigurationProperties("adapters.smoot") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps smootBidderDeps(BidderConfigurationProperties smootConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(smootConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SmootBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/SparteoConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/SparteoConfiguration.java new file mode 100644 index 00000000000..5934319d2db --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/SparteoConfiguration.java @@ -0,0 +1,42 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.sparteo.SparteoBidder; +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/sparteo.yaml", + factory = YamlPropertySourceFactory.class) +public class SparteoConfiguration { + + private static final String BIDDER_NAME = "sparteo"; + + @Bean("sparteoConfigurationProperties") + @ConfigurationProperties("adapters.sparteo") + public BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + public BidderDeps sparteoBidderDeps(BidderConfigurationProperties sparteoConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(sparteoConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new SparteoBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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/StartioBidderConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java new file mode 100644 index 00000000000..1cb3bda1cf3 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/StartioBidderConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.startio.StartioBidder; +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/startio.yaml", factory = YamlPropertySourceFactory.class) +public class StartioBidderConfiguration { + + private static final String BIDDER_NAME = "startio"; + + @Bean("startioConfigurationProperties") + @ConfigurationProperties("adapters.startio") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps startioBidderDeps(BidderConfigurationProperties startioConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(startioConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new StartioBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} 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..49369a2ddac 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 @@ -2,7 +2,6 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.stroeercore.StroeerCoreBidder; -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; @@ -14,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/stroeercore.yaml", factory = YamlPropertySourceFactory.class) @@ -31,13 +30,12 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps stroeercoreBidderDeps(BidderConfigurationProperties stroeercoreConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - CurrencyConversionService currencyConversionService, JacksonMapper mapper) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(stroeercoreConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new StroeerCoreBidder(config.getEndpoint(), mapper, currencyConversionService)) + .bidderCreator(config -> new StroeerCoreBidder(config.getEndpoint(), mapper)) .assemble(); } } 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..6c7b8dedae6 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 @@ -41,4 +41,3 @@ BidderDeps tappxBidderDeps(BidderConfigurationProperties tappxConfigurationPrope .assemble(); } } - 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..6feae487f51 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) @@ -39,4 +39,3 @@ BidderDeps unicornBidderDeps(BidderConfigurationProperties unicornConfigurationP .assemble(); } } - 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/ZentotemConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/ZentotemConfiguration.java new file mode 100644 index 00000000000..1578640e0ad --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/ZentotemConfiguration.java @@ -0,0 +1,41 @@ +package org.prebid.server.spring.config.bidder; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.zentotem.ZentotemBidder; +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/zentotem.yaml", factory = YamlPropertySourceFactory.class) +public class ZentotemConfiguration { + + private static final String BIDDER_NAME = "zentotem"; + + @Bean("zentotemConfigurationProperties") + @ConfigurationProperties("adapters.zentotem") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps zentotemBidderDeps(BidderConfigurationProperties zentotemConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(zentotemConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new ZentotemBidder(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..a13e1aef3ec 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; @@ -51,6 +51,8 @@ public class BidderConfigurationProperties { private Ortb ortb; + private long tmaxDeductionMs; + private final Class selfClass; public BidderConfigurationProperties() { 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/UsersyncBidderRegulationScopeProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncBidderRegulationScopeProperties.java new file mode 100644 index 00000000000..49ab5fd74b4 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/model/usersync/UsersyncBidderRegulationScopeProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.spring.config.bidder.model.usersync; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class UsersyncBidderRegulationScopeProperties { + + boolean gdpr; + + List gppSid; +} 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..a93fac2f41c 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 @@ -19,4 +19,6 @@ public class UsersyncConfigurationProperties { UsersyncMethodConfigurationProperties redirect; UsersyncMethodConfigurationProperties iframe; + + UsersyncBidderRegulationScopeProperties skipwhen; } 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..8780225ff9f 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,9 +28,11 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope metaInfo.getDoohMediaTypes(), metaInfo.getSupportedVendors(), metaInfo.getVendorId(), + metaInfo.getCurrencyAccepted(), configurationProperties.getPbsEnforcesCcpa(), configurationProperties.getModifyingVastXmlAllowed(), configurationProperties.getEndpointCompression(), - configurationProperties.getOrtb()); + configurationProperties.getOrtb(), + configurationProperties.getTmaxDeductionMs()); } } diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/TeqblazeConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/util/TeqblazeConfiguration.java new file mode 100644 index 00000000000..b780582cf11 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/TeqblazeConfiguration.java @@ -0,0 +1,39 @@ +package org.prebid.server.spring.config.bidder.util; + +import org.prebid.server.bidder.BidderDeps; +import org.prebid.server.bidder.teqblaze.TeqblazeBidder; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; +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/teqblaze.yaml", factory = YamlPropertySourceFactory.class) +public class TeqblazeConfiguration { + + private static final String BIDDER_NAME = "teqblaze"; + + @Bean("teqblazeConfigurationProperties") + @ConfigurationProperties("adapters.teqblaze") + BidderConfigurationProperties configurationProperties() { + return new BidderConfigurationProperties(); + } + + @Bean + BidderDeps teqblazeBidderDeps(BidderConfigurationProperties teqblazeConfigurationProperties, + @NotBlank @Value("${external-url}") String externalUrl, + JacksonMapper mapper) { + + return BidderDepsAssembler.forBidder(BIDDER_NAME) + .withConfig(teqblazeConfigurationProperties) + .usersyncerCreator(UsersyncerCreator.create(externalUrl)) + .bidderCreator(config -> new TeqblazeBidder(config.getEndpoint(), mapper)) + .assemble(); + } +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java index 462bd3d3cdc..b5606c719b9 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/util/UsersyncerCreator.java @@ -6,6 +6,7 @@ import org.prebid.server.bidder.UsersyncUtil; import org.prebid.server.bidder.Usersyncer; import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource; +import org.prebid.server.spring.config.bidder.model.usersync.UsersyncBidderRegulationScopeProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncConfigurationProperties; import org.prebid.server.spring.config.bidder.model.usersync.UsersyncMethodConfigurationProperties; import org.prebid.server.util.HttpUtil; @@ -30,13 +31,16 @@ private static Usersyncer createAndValidate(UsersyncConfigurationProperties user String externalUrl) { final String cookieFamilyName = usersync.getCookieFamilyName(); + final UsersyncBidderRegulationScopeProperties skipwhenConfig = usersync.getSkipwhen(); return Usersyncer.of( usersync.getEnabled(), cookieFamilyName, cookieFamilySource, toMethod(UsersyncMethodType.IFRAME, usersync.getIframe(), cookieFamilyName, externalUrl), - toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName, externalUrl)); + toMethod(UsersyncMethodType.REDIRECT, usersync.getRedirect(), cookieFamilyName, externalUrl), + skipwhenConfig != null && skipwhenConfig.isGdpr(), + skipwhenConfig == null ? null : skipwhenConfig.getGppSid()); } private static UsersyncMethod toMethod(UsersyncMethodType type, diff --git a/src/main/java/org/prebid/server/spring/config/database/ConnectionPoolConfigurationFactory.java b/src/main/java/org/prebid/server/spring/config/database/ConnectionPoolConfigurationFactory.java deleted file mode 100644 index cba0282f72e..00000000000 --- a/src/main/java/org/prebid/server/spring/config/database/ConnectionPoolConfigurationFactory.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.prebid.server.spring.config.database; - -import io.vertx.core.json.JsonObject; -import org.prebid.server.spring.config.database.model.ConnectionPoolSettings; - -public interface ConnectionPoolConfigurationFactory { - - JsonObject create(String databaseUrl, ConnectionPoolSettings connectionPoolSettings); -} 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/DatabaseUrlFactory.java b/src/main/java/org/prebid/server/spring/config/database/DatabaseUrlFactory.java deleted file mode 100644 index 0c1828fb9b1..00000000000 --- a/src/main/java/org/prebid/server/spring/config/database/DatabaseUrlFactory.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.prebid.server.spring.config.database; - -public interface DatabaseUrlFactory { - - String createUrl(String host, int port, String databaseName); -} 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..8542f707911 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 @@ -1,12 +1,7 @@ package org.prebid.server.spring.config.database.model; -import lombok.AllArgsConstructor; - -@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..a30b61f9fdd 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 @@ -4,6 +4,7 @@ import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.ScheduledReporter; import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.Slf4jReporter; import com.codahale.metrics.graphite.Graphite; import com.codahale.metrics.graphite.GraphiteReporter; import com.codahale.metrics.jvm.GarbageCollectorMetricSet; @@ -11,17 +12,16 @@ 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; +import org.prebid.server.auction.HooksMetricsService; import org.prebid.server.metric.AccountMetricsVerbosityResolver; import org.prebid.server.metric.CounterType; 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.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -31,10 +31,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 +46,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 +95,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,11 +135,9 @@ AccountMetricsVerbosityResolver accountMetricsVerbosity(AccountsProperties accou accountsProperties.getDetailedVerbosity()); } - @PostConstruct - void registerReporterCloseHooks() { - reporters.stream() - .map(CloseableAdapter::new) - .forEach(closeable -> vertx.getOrCreateContext().addCloseHook(closeable)); + @Bean + HooksMetricsService hooksMetricsService(Metrics metrics) { + return new HooksMetricsService(metrics); } @Component @@ -199,6 +204,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/CacheDefaultTtlProperties.java b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java new file mode 100644 index 00000000000..2a3e36b6ef1 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java @@ -0,0 +1,15 @@ +package org.prebid.server.spring.config.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class CacheDefaultTtlProperties { + + Integer bannerTtl; + + Integer videoTtl; + + Integer audioTtl; + + Integer nativeTtl; +} 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..3e1181b5fe5 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 @@ -50,4 +50,3 @@ public class ExternalConversionProperties { @NotNull JacksonMapper mapper; } - diff --git a/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java new file mode 100644 index 00000000000..54dbd81a5a9 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java @@ -0,0 +1,51 @@ +package org.prebid.server.spring.config.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Validated +@Data +@NoArgsConstructor +public class FileSyncerProperties { + + private Type type = Type.REMOTE; + + @NotBlank + private String downloadUrl; + + @NotBlank + private String saveFilepath; + + @NotBlank + private String tmpFilepath; + + @Min(1) + private Integer retryCount; + + @Min(1) + private Long retryIntervalMs; + + private ExponentialBackoffProperties retry; + + @NotNull + @Min(1) + private Long timeoutMs; + + @NotNull + private Long updateIntervalMs; + + private boolean checkSize; + + @NotNull + private HttpClientProperties httpClient; + + public enum Type { + + LOCAL, REMOTE + } +} 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 deleted file mode 100644 index a354dd46b3e..00000000000 --- a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java +++ /dev/null @@ -1,42 +0,0 @@ -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.NotBlank; -import javax.validation.constraints.NotNull; - -@Validated -@Data -@NoArgsConstructor -public class RemoteFileSyncerProperties { - - @NotBlank - private String downloadUrl; - - @NotBlank - private String saveFilepath; - - @NotBlank - private String tmpFilepath; - - @NotNull - @Min(1) - private Integer retryCount; - - @NotNull - @Min(1) - private Long retryIntervalMs; - - @NotNull - @Min(1) - private Long timeoutMs; - - @NotNull - private Long updateIntervalMs; - - @NotNull - private HttpClientProperties httpClient; -} 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..06d6f6d7264 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 @@ -27,4 +27,3 @@ public ExponentialBackoffRetryPolicy toPolicy() { return ExponentialBackoffRetryPolicy.of(delayMillis, maxDelayMillis, factor, jitter); } } - 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..a5938aab396 --- /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( + @Value("${admin-endpoints.storedrequest.path}") String path, + @Value("${admin-endpoints.storedrequest.on-application-port}") boolean isOnApplicationPort, + @Value("${admin-endpoints.storedrequest.protected}") boolean isProtected, + SettingsCache settingsCache, + JacksonMapper mapper) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new SettingsCacheNotificationHandler(path, settingsCache, mapper)); + } + + @Bean + @ConditionalOnExpression("${settings.in-memory-cache.notification-endpoints-enabled:false}" + + " and ${admin-endpoints.storedrequest-amp.enabled} == true") + AdminResource ampCacheNotificationEndpoint( + @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, + SettingsCache ampSettingsCache, + JacksonMapper mapper) { + + return new AdminResourceWrapper( + path, + isOnApplicationPort, + isProtected, + new SettingsCacheNotificationHandler(path, ampSettingsCache, mapper)); + } + + @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..c6ae167ae94 --- /dev/null +++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java @@ -0,0 +1,478 @@ +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.HooksMetricsService; +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.timeout.TimeoutFactory; +import org.prebid.server.handler.BidderParamHandler; +import org.prebid.server.handler.CookieSyncHandler; +import org.prebid.server.handler.ExceptionHandler; +import org.prebid.server.handler.GetVtrackHandler; +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.PostVtrackHandler; +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.AuctionHandler; +import org.prebid.server.handler.openrtb2.VideoHandler; +import org.prebid.server.health.HealthChecker; +import org.prebid.server.health.PeriodicHealthChecker; +import org.prebid.server.hooks.execution.HookStageExecutor; +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) + .setKeyCertOptions(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() + .addRelativeOrigin(".*") + .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 + AuctionHandler openrtbAuctionHandler( + ExchangeService exchangeService, + SkippedAuctionService skippedAuctionService, + AuctionRequestFactory auctionRequestFactory, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + HooksMetricsService hooksMetricsService, + Clock clock, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, + JacksonMapper mapper) { + + return new AuctionHandler( + logSamplingRate, + auctionRequestFactory, + exchangeService, + skippedAuctionService, + analyticsReporter, + metrics, + hooksMetricsService, + clock, + httpInteractionLogger, + prebidVersionProvider, + hookStageExecutor, + mapper); + } + + @Bean + AmpHandler openrtbAmpHandler( + AmpRequestFactory ampRequestFactory, + ExchangeService exchangeService, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + HooksMetricsService hooksMetricsService, + Clock clock, + BidderCatalog bidderCatalog, + AmpProperties ampProperties, + AmpResponsePostProcessor ampResponsePostProcessor, + HttpInteractionLogger httpInteractionLogger, + PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, + JacksonMapper mapper) { + + return new AmpHandler( + ampRequestFactory, + exchangeService, + analyticsReporter, + metrics, + hooksMetricsService, + clock, + bidderCatalog, + ampProperties.getCustomTargetingSet(), + ampResponsePostProcessor, + httpInteractionLogger, + prebidVersionProvider, + hookStageExecutor, + mapper, + logSamplingRate); + } + + @Bean + VideoHandler openrtbVideoHandler( + VideoRequestFactory videoRequestFactory, + VideoResponseFactory videoResponseFactory, + ExchangeService exchangeService, + CoreCacheService coreCacheService, + AnalyticsReporterDelegator analyticsReporter, + Metrics metrics, + HooksMetricsService hooksMetricsService, + Clock clock, + PrebidVersionProvider prebidVersionProvider, + HookStageExecutor hookStageExecutor, + JacksonMapper mapper) { + + return new VideoHandler( + videoRequestFactory, + videoResponseFactory, + exchangeService, + coreCacheService, analyticsReporter, + metrics, + hooksMetricsService, + clock, + prebidVersionProvider, + hookStageExecutor, + 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 + PostVtrackHandler postVtrackHandler( + @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 PostVtrackHandler( + defaultTimeoutMs, + allowUnknownBidder, + modifyVastForUnknownBidder, + applicationSettings, + bidderCatalog, + coreCacheService, + timeoutFactory, + mapper); + } + + @Bean + GetVtrackHandler getVtrackHandler(@Value("${vtrack.default-timeout-ms}") int defaultTimeoutMs, + CoreCacheService coreCacheService, + TimeoutFactory timeoutFactory) { + + return new GetVtrackHandler(defaultTimeoutMs, coreCacheService, timeoutFactory); + } + + @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..21755842d2a 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; @@ -64,7 +63,7 @@ public static boolean isValidPrice(Price price) { public static boolean shouldConvertBidFloor(Price price, String bidderCurrency) { return isValidPrice(price) - && !StringUtils.equals(price.getCurrency(), bidderCurrency); + && !StringUtils.equalsIgnoreCase(price.getCurrency(), bidderCurrency); } public static PriceFloorInfo resolvePriceFloor(Bid bid, BidRequest bidRequest) { @@ -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..e08a276c6fa 100644 --- a/src/main/java/org/prebid/server/util/HttpUtil.java +++ b/src/main/java/org/prebid/server/util/HttpUtil.java @@ -6,13 +6,12 @@ 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.apache.commons.validator.routines.UrlValidator; 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 +20,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 +43,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 +60,32 @@ 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"); + public static final CharSequence SEC_CH_UA_PLATFORM_VERSION = + HttpHeaders.createOptimized("Sec-CH-UA-Platform-Version"); + public static final CharSequence SEC_CH_UA_ARCH = HttpHeaders.createOptimized("Sec-CH-UA-Arch"); + public static final CharSequence SEC_CH_UA_MODEL = HttpHeaders.createOptimized("Sec-CH-UA-Model"); + public static final CharSequence SEC_CH_UA_FULL_VERSION_LIST = + HttpHeaders.createOptimized("Sec-CH-UA-Full-Version-List"); + public static final String MACROS_OPEN = "{{"; + public static final String MACROS_CLOSE = "}}"; - private static final String BASIC_AUTH_PATTERN = "Basic %s"; + private static final UrlValidator URL_VALIDAROR = UrlValidator.getInstance(); private HttpUtil() { } @@ -93,7 +93,12 @@ private HttpUtil() { /** * Checks the input string for using as URL. */ + @Deprecated public static String validateUrl(String url) { + if (containsMacrosses(url)) { + return url; + } + try { return new URL(url).toString(); } catch (MalformedURLException e) { @@ -101,6 +106,19 @@ public static String validateUrl(String url) { } } + public static String validateUrlSyntax(String url) { + if (containsMacrosses(url) || URL_VALIDAROR.isValid(url)) { + return url; + } + + throw new IllegalArgumentException("URL supplied is not valid: " + 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 +156,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 +192,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 +215,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/ResourceUtil.java b/src/main/java/org/prebid/server/util/ResourceUtil.java index ca2a7ef72ea..898dd29f385 100644 --- a/src/main/java/org/prebid/server/util/ResourceUtil.java +++ b/src/main/java/org/prebid/server/util/ResourceUtil.java @@ -1,6 +1,6 @@ package org.prebid.server.util; -import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.io.IOUtils; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/org/prebid/server/util/StreamUtil.java b/src/main/java/org/prebid/server/util/StreamUtil.java index 0a48bede9e7..998e6f669b6 100644 --- a/src/main/java/org/prebid/server/util/StreamUtil.java +++ b/src/main/java/org/prebid/server/util/StreamUtil.java @@ -1,7 +1,11 @@ package org.prebid.server.util; +import java.util.HashSet; import java.util.Iterator; +import java.util.Set; import java.util.Spliterator; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -17,4 +21,9 @@ public static Stream asStream(Spliterator spliterator) { public static Stream asStream(Iterator iterator) { return StreamSupport.stream(IterableUtil.iterable(iterator).spliterator(), false); } + + public static Predicate distinctBy(Function keyExtractor) { + final Set seen = new HashSet<>(); + return value -> seen.add(keyExtractor.apply(value)); + } } diff --git a/src/main/java/org/prebid/server/util/VersionInfo.java b/src/main/java/org/prebid/server/util/VersionInfo.java index b163be55f3c..3ddc9b52b74 100644 --- a/src/main/java/org/prebid/server/util/VersionInfo.java +++ b/src/main/java/org/prebid/server/util/VersionInfo.java @@ -1,11 +1,10 @@ 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 +30,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(); @@ -48,8 +47,7 @@ private static String extractVersion(String buildVersion) { return versionMatcher.lookingAt() ? versionMatcher.group() : null; } - @AllArgsConstructor(staticName = "of") - @Value + @Value(staticConstructor = "of") private static class Revision { @JsonProperty("git.commit.id") diff --git a/src/main/java/org/prebid/server/util/algorithms/random/RandomAnyWeightedEntrySupplier.java b/src/main/java/org/prebid/server/util/algorithms/random/RandomAnyWeightedEntrySupplier.java index 8ce28177988..37f3337b339 100644 --- a/src/main/java/org/prebid/server/util/algorithms/random/RandomAnyWeightedEntrySupplier.java +++ b/src/main/java/org/prebid/server/util/algorithms/random/RandomAnyWeightedEntrySupplier.java @@ -59,4 +59,3 @@ private E getEntry(Iterable entries, long entryNumber) { throw new AssertionError(); } } - diff --git a/src/main/java/org/prebid/server/util/algorithms/random/RandomPositiveWeightedEntrySupplier.java b/src/main/java/org/prebid/server/util/algorithms/random/RandomPositiveWeightedEntrySupplier.java index 675dba5aa4f..595fc52b1df 100644 --- a/src/main/java/org/prebid/server/util/algorithms/random/RandomPositiveWeightedEntrySupplier.java +++ b/src/main/java/org/prebid/server/util/algorithms/random/RandomPositiveWeightedEntrySupplier.java @@ -17,4 +17,3 @@ protected int weight(E entry) { throw new IllegalArgumentException("Entry weight must be greater than 0."); } } - diff --git a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java index ea8a841df95..9cbcae877da 100644 --- a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java +++ b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java @@ -14,10 +14,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.TreeSet; /** * Priority order for four column rule sets: @@ -174,7 +174,7 @@ private static List generateWildcardsIndices(Iterable toSet(Iterable iterable) { - return iterable instanceof Set set ? set : fill(new HashSet<>(), iterable); + return fill(new TreeSet<>(String.CASE_INSENSITIVE_ORDER), iterable); } private static > C fill(C destination, Iterable source) { 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/BidderParamValidator.java b/src/main/java/org/prebid/server/validation/BidderParamValidator.java index 98dea900d5c..71a21b13341 100644 --- a/src/main/java/org/prebid/server/validation/BidderParamValidator.java +++ b/src/main/java/org/prebid/server/validation/BidderParamValidator.java @@ -7,7 +7,6 @@ import com.networknt.schema.SpecVersion; import com.networknt.schema.ValidationMessage; import org.apache.commons.collections4.map.CaseInsensitiveMap; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.json.EncodeException; @@ -88,7 +87,7 @@ public static BidderParamValidator create(BidderCatalog bidderCatalog, final Map bidderRawSchemas = new LinkedHashMap<>(); bidderCatalog.names().forEach(bidder -> bidderRawSchemas.put( - bidder, createSchemaNode(schemaDirectory, maybeResolveAlias(bidderCatalog, bidder), mapper))); + bidder, createSchemaNode(bidderCatalog, schemaDirectory, bidder, mapper))); return new BidderParamValidator(toBidderSchemas(bidderRawSchemas), toSchemas(bidderRawSchemas, mapper)); } @@ -120,8 +119,20 @@ private static JsonSchema toBidderSchema(JsonNode schema, String bidder) { return result; } - private static String maybeResolveAlias(BidderCatalog bidderCatalog, String bidder) { - return ObjectUtils.defaultIfNull(bidderCatalog.bidderInfoByName(bidder).getAliasOf(), bidder); + private static JsonNode createSchemaNode(BidderCatalog bidderCatalog, + String schemaDirectory, + String bidder, + JacksonMapper mapper) { + + try { + return createSchemaNode(schemaDirectory, bidder, mapper); + } catch (IllegalArgumentException e) { + final String parentBidder = bidderCatalog.bidderInfoByName(bidder).getAliasOf(); + if (parentBidder != null) { + return createSchemaNode(schemaDirectory, parentBidder, mapper); + } + throw e; + } } private static JsonNode createSchemaNode(String schemaDirectory, String bidder, JacksonMapper mapper) { 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..9a31aa6e52d --- /dev/null +++ b/src/main/java/org/prebid/server/validation/ImpValidator.java @@ -0,0 +1,672 @@ +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.auction.aliases.BidderAliases; +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 requestAliases, + 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); + + final BidderAliases aliases = BidderAliases.of(requestAliases, null, bidderCatalog); + + 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, + BidderAliases 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.resolveBidder(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, + BidderAliases 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.resolveBidder(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, + BidderAliases 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, + BidderAliases 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.resolveBidder(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..958c59d3a5a 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -3,45 +3,27 @@ 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.collections4.map.CaseInsensitiveMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; +import org.prebid.server.auction.aliases.AlternateBidder; +import org.prebid.server.auction.aliases.AlternateBidderCodesConfig; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.auction.model.debug.DebugContext; 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 +31,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 +41,25 @@ 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.settings.model.Account; 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,47 +70,51 @@ 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; private final boolean enabledStrictAppSiteDoohValidation; + private final boolean failOnDisabledBidders; + private final boolean failOnUnknownBidders; /** * Constructs a RequestValidator that will use the BidderParamValidator passed in order to validate all critical * properties of bidRequest. */ public RequestValidator(BidderCatalog bidderCatalog, - BidderParamValidator bidderParamValidator, - Metrics metrics, + ImpValidator impValidator, Metrics metrics, JacksonMapper mapper, double logSamplingRate, - boolean enabledStrictAppSiteDoohValidation) { + boolean enabledStrictAppSiteDoohValidation, + boolean failOnDisabledBidders, + boolean failOnUnknownBidders) { 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; this.enabledStrictAppSiteDoohValidation = enabledStrictAppSiteDoohValidation; + this.failOnDisabledBidders = failOnDisabledBidders; + this.failOnUnknownBidders = failOnUnknownBidders; } /** * Validates the {@link BidRequest} against a list of validation checks, however, reports only one problem * at a time. */ - public ValidationResult validate(BidRequest bidRequest, HttpRequestContext httpRequestContext) { + public ValidationResult validate(Account account, + BidRequest bidRequest, + HttpRequestContext httpRequestContext, + DebugContext debugContext) { + final List warnings = new ArrayList<>(); + final boolean isDebugEnabled = debugContext != null && debugContext.isDebugEnabled(); try { if (StringUtils.isBlank(bidRequest.getId())) { throw new ValidationException("request missing required field: \"id\""); @@ -157,11 +137,19 @@ public ValidationResult validate(BidRequest bidRequest, HttpRequestContext httpR if (targeting != null) { validateTargeting(targeting); } - aliases = ObjectUtils.defaultIfNull(extRequestPrebid.getAliases(), Collections.emptyMap()); - validateAliases(aliases); + aliases = new CaseInsensitiveMap<>(MapUtils.emptyIfNull(extRequestPrebid.getAliases())); + validateAliases(aliases, warnings, account); validateAliasesGvlIds(extRequestPrebid, aliases); - validateBidAdjustmentFactors(extRequestPrebid.getBidadjustmentfactors(), aliases); - validateExtBidPrebidData(extRequestPrebid.getData(), aliases); + validateAlternateBidderCodes(extRequestPrebid.getAlternateBidderCodes(), aliases); + + final AlternateBidderCodesConfig alternateBidderCodesConfig = ObjectUtils.defaultIfNull( + extRequestPrebid.getAlternateBidderCodes(), + account == null ? null : account.getAlternateBidderCodes()); + + final BidderAliases bidderAliases = BidderAliases.of( + aliases, null, bidderCatalog, alternateBidderCodesConfig); + validateBidAdjustmentFactors(extRequestPrebid.getBidadjustmentfactors(), bidderAliases); + validateExtBidPrebidData(extRequestPrebid.getData(), aliases, isDebugEnabled, warnings); validateSchains(extRequestPrebid.getSchains()); } @@ -186,9 +174,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")); @@ -243,7 +229,8 @@ private void validateCur(List currencies) throws ValidationException { private void validateAliasesGvlIds(ExtRequestPrebid extRequestPrebid, Map aliases) throws ValidationException { - final Map aliasGvlIds = MapUtils.emptyIfNull(extRequestPrebid.getAliasgvlids()); + final Map aliasGvlIds = new CaseInsensitiveMap<>(MapUtils.emptyIfNull( + extRequestPrebid.getAliasgvlids())); for (Map.Entry aliasToGvlId : aliasGvlIds.entrySet()) { @@ -264,9 +251,25 @@ private void validateAliasesGvlIds(ExtRequestPrebid extRequestPrebid, } } - private void validateBidAdjustmentFactors(ExtRequestBidAdjustmentFactors adjustmentFactors, + private void validateAlternateBidderCodes(AlternateBidderCodesConfig alternateBidderCodesConfig, Map aliases) throws ValidationException { + final Map alternateBidders = Optional.ofNullable(alternateBidderCodesConfig) + .map(AlternateBidderCodesConfig::getBidders) + .orElse(Collections.emptyMap()); + + for (Map.Entry alternateBidder : alternateBidders.entrySet()) { + final String bidder = alternateBidder.getKey(); + if (isUnknownBidderOrAlias(bidder, aliases)) { + throw new ValidationException( + "request.ext.prebid.alternatebiddercodes.bidders.%s is not a known bidder or alias", bidder); + } + } + } + + private void validateBidAdjustmentFactors(ExtRequestBidAdjustmentFactors adjustmentFactors, + BidderAliases bidderAliases) throws ValidationException { + final Map bidderAdjustments = adjustmentFactors != null ? adjustmentFactors.getAdjustments() : Collections.emptyMap(); @@ -274,7 +277,10 @@ private void validateBidAdjustmentFactors(ExtRequestBidAdjustmentFactors adjustm for (Map.Entry bidderAdjustment : bidderAdjustments.entrySet()) { final String bidder = bidderAdjustment.getKey(); - if (isUnknownBidderOrAlias(bidder, aliases)) { + if (!bidderCatalog.isValidName(bidder) + && !bidderAliases.isAliasDefined(bidder) + && !bidderAliases.isKnownAlternateBidderCode(bidder)) { + throw new ValidationException( "request.ext.prebid.bidadjustmentfactors.%s is not a known bidder or alias", bidder); } @@ -297,18 +303,21 @@ private void validateBidAdjustmentFactors(ExtRequestBidAdjustmentFactors adjustm for (Map.Entry> entry : adjustmentsMediaTypeFactors.entrySet()) { - validateBidAdjustmentFactorsByMediatype(entry.getKey(), entry.getValue(), aliases); + validateBidAdjustmentFactorsByMediatype(entry.getKey(), entry.getValue(), bidderAliases); } } private void validateBidAdjustmentFactorsByMediatype(ImpMediaType mediaType, Map bidderAdjustments, - Map aliases) throws ValidationException { + BidderAliases bidderAliases) throws ValidationException { for (Map.Entry bidderAdjustment : bidderAdjustments.entrySet()) { final String bidder = bidderAdjustment.getKey(); - if (isUnknownBidderOrAlias(bidder, aliases)) { + if (!bidderCatalog.isValidName(bidder) + && !bidderAliases.isAliasDefined(bidder) + && !bidderAliases.isKnownAlternateBidderCode(bidder)) { + throw new ValidationException( "request.ext.prebid.bidadjustmentfactors.%s.%s is not a known bidder or alias", mediaType, bidder); @@ -352,17 +361,22 @@ private void validateSchains(List schains) throws Valida } } - private void validateExtBidPrebidData(ExtRequestPrebidData data, Map aliases) - throws ValidationException { + private void validateExtBidPrebidData(ExtRequestPrebidData data, + Map aliases, + boolean isDebugEnabled, + List warnings) throws ValidationException { + if (data != null) { - validateEidPermissions(data.getEidPermissions(), aliases); + validateEidPermissions(data.getEidPermissions(), aliases, isDebugEnabled, warnings); } } private void validateEidPermissions(List eidPermissions, - Map aliases) throws ValidationException { + Map aliases, + boolean isDebugEnabled, + List warnings) throws ValidationException { - if (eidPermissions == null) { + if (CollectionUtils.isEmpty(eidPermissions)) { return; } @@ -371,30 +385,44 @@ private void validateEidPermissions(List eid throw new ValidationException("request.ext.prebid.data.eidpermissions[i] can't be null"); } - validateEidPermissionSource(eidPermission.getSource()); - validateEidPermissionBidders(eidPermission.getBidders(), aliases); + validateEidPermissionCriteria( + eidPermission.getInserter(), + eidPermission.getSource(), + eidPermission.getMatcher(), + eidPermission.getMm()); + + validateEidPermissionBidders(eidPermission.getBidders(), aliases, isDebugEnabled, warnings); } } - private void validateEidPermissionSource(String source) throws ValidationException { - if (StringUtils.isEmpty(source)) { - throw new ValidationException("Missing required value request.ext.prebid.data.eidPermissions[].source"); + private void validateEidPermissionCriteria(String inserter, + String source, + String matcher, + Integer mm) throws ValidationException { + + if (StringUtils.isAllEmpty(inserter, source, matcher) && mm == null) { + throw new ValidationException("Missing required parameter(s) in request.ext.prebid.data.eidPermissions[]. " + + "Either one or a combination of inserter, source, matcher, or mm should be defined."); } } private void validateEidPermissionBidders(List bidders, - Map aliases) throws ValidationException { + Map aliases, + boolean isDebugEnabled, + List warnings) throws ValidationException { if (CollectionUtils.isEmpty(bidders)) { throw new ValidationException("request.ext.prebid.data.eidpermissions[].bidders[] required values" + " but was empty or null"); } - for (String bidder : bidders) { - if (!bidderCatalog.isValidName(bidder) && !bidderCatalog.isValidName(aliases.get(bidder)) - && ObjectUtils.notEqual(bidder, ASTERISK)) { - throw new ValidationException( - "request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: '%s'", bidder); + if (isDebugEnabled) { + for (String bidder : bidders) { + if (!bidderCatalog.isValidName(bidder) && !bidderCatalog.isValidName(aliases.get(bidder)) + && ObjectUtils.notEqual(bidder, ASTERISK)) { + warnings.add("request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: '%s'" + .formatted(bidder)); + } } } } @@ -530,19 +558,35 @@ private static void validateGranularityRangeIncrement(ExtGranularityRange range) * Validates aliases. Throws {@link ValidationException} in cases when alias points to invalid bidder or when alias * is equals to itself. */ - private void validateAliases(Map aliases) throws ValidationException { + private void validateAliases(Map aliases, List warnings, + Account account) throws ValidationException { + for (final Map.Entry aliasToBidder : aliases.entrySet()) { final String alias = aliasToBidder.getKey(); final String coreBidder = aliasToBidder.getValue(); if (!bidderCatalog.isValidName(coreBidder)) { - throw new ValidationException( - "request.ext.prebid.aliases.%s refers to unknown bidder: %s".formatted(alias, coreBidder)); - } - if (!bidderCatalog.isActive(coreBidder)) { - throw new ValidationException( - "request.ext.prebid.aliases.%s refers to disabled bidder: %s".formatted(alias, coreBidder)); + metrics.updateUnknownBidderMetric(account); + + final String message = "request.ext.prebid.aliases.%s refers to unknown bidder: %s".formatted(alias, + coreBidder); + if (failOnUnknownBidders) { + throw new ValidationException(message); + } else { + warnings.add(message); + } + } else if (!bidderCatalog.isActive(coreBidder)) { + metrics.updateDisabledBidderMetric(account); + + final String message = "request.ext.prebid.aliases.%s refers to disabled bidder: %s".formatted(alias, + coreBidder); + if (failOnDisabledBidders) { + throw new ValidationException(message); + } else { + warnings.add(message); + } } - if (alias.equals(coreBidder)) { + + if (alias.equalsIgnoreCase(coreBidder)) { throw new ValidationException(""" request.ext.prebid.aliases.%s defines a no-op alias. \ Choose a different alias, or remove this entry""".formatted(alias)); @@ -610,19 +654,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 +691,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..660d5785395 100644 --- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java +++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java @@ -1,30 +1,25 @@ 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.aliases.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.auction.model.BidRejection; 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 +28,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. @@ -49,40 +40,33 @@ public class ResponseBidValidator { private static final Logger logger = LoggerFactory.getLogger(ResponseBidValidator.class); - private static final ConditionalLogger UNRELATED_BID_LOGGER = new ConditionalLogger("not_matched_bid", logger); - private static final ConditionalLogger SECURE_CREATIVE_LOGGER = new ConditionalLogger("secure_creatives_validation", - logger); - private static final ConditionalLogger CREATIVE_SIZE_LOGGER = new ConditionalLogger("creative_size_validation", - logger); + private static final ConditionalLogger unrelatedBidLogger = + new ConditionalLogger("not_matched_bid", logger); + private static final ConditionalLogger secureCreativeLogger = + new ConditionalLogger("secure_creatives_validation", logger); + private static final ConditionalLogger creativeSizeLogger = + new ConditionalLogger("creative_size_validation", logger); + private static final ConditionalLogger alternateBidderCodeLogger = + new ConditionalLogger("alternate_bidder_code_validation", logger); 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,23 +78,36 @@ 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 { validateCommonFields(bid); validateTypeSpecific(bidderBid, bidder); validateCurrency(bidderBid.getBidCurrency()); + validateSeat(bidderBid, bidder, account, bidRejectionTracker, aliases); final Imp correspondingImp = findCorrespondingImp(bid, bidRequest); if (bidderBid.getType() == BidType.banner) { - warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases)); + warnings.addAll(validateBannerFields( + bidderBid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); } - if (dealsEnabled) { - validateDealsFor(bidderBid, bidRequest, bidder, aliases, warnings); - } + warnings.addAll(validateSecureMarkup( + bidderBid, + bidder, + bidRequest, + account, + correspondingImp, + aliases, + bidRejectionTracker)); - warnings.addAll(validateSecureMarkup(bid, bidder, bidRequest, account, correspondingImp, aliases)); } catch (ValidationException e) { return ValidationResult.error(warnings, e.getMessage()); } @@ -156,6 +153,26 @@ private static void validateCurrency(String currency) throws ValidationException } } + private void validateSeat(BidderBid bid, + String bidder, + Account account, + BidRejectionTracker bidRejectionTracker, + BidderAliases bidderAliases) throws ValidationException { + + final String seat = bid.getSeat(); + if (seat != null + && !StringUtils.equalsIgnoreCase(bidder, seat) + && !bidderAliases.isAllowedAlternateBidderCode(bidder, seat)) { + + final String message = "invalid bidder code %s was set by the adapter %s for the account %s" + .formatted(bid.getSeat(), bidder, account.getId()); + bidRejectionTracker.reject(BidRejection.of(bid, BidRejectionReason.RESPONSE_REJECTED_GENERAL)); + metrics.updateSeatValidationMetrics(bidder); + alternateBidderCodeLogger.warn(message, logSamplingRate); + throw new ValidationException(message); + } + } + private Imp findCorrespondingImp(Bid bid, BidRequest bidRequest) throws ValidationException { return bidRequest.getImp().stream() .filter(imp -> Objects.equals(imp.getId(), bid.getImpid())) @@ -165,21 +182,22 @@ private Imp findCorrespondingImp(Bid bid, BidRequest bidRequest) throws Validati } private ValidationException exceptionAndLogOnePercent(String message) { - UNRELATED_BID_LOGGER.warn(message, logSamplingRate); + unrelatedBidLogger.warn(message, logSamplingRate); return new ValidationException(message); } - private List validateBannerFields(Bid bid, + private List validateBannerFields(BidderBid bidderBid, String bidder, 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) { final Format maxSize = maxSizeForBanner(correspondingImp); - + final Bid bid = bidderBid.getBid(); if (bannerSizeIsNotValid(bid, maxSize)) { final String accountId = account.getId(); final String message = """ @@ -200,7 +218,11 @@ private List validateBannerFields(Bid bid, bannerMaxSizeEnforcement, metricName -> metrics.updateSizeValidationMetrics( aliases.resolveBidder(bidder), accountId, metricName), - CREATIVE_SIZE_LOGGER, message); + creativeSizeLogger, + message, + bidRejectionTracker, + bidderBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED); } } return Collections.emptyList(); @@ -239,12 +261,13 @@ private static boolean bannerSizeIsNotValid(Bid bid, Format maxSize) { || bidH == null || bidH > maxSize.getH(); } - private List validateSecureMarkup(Bid bid, + private List validateSecureMarkup(BidderBid bidderBid, String bidder, BidRequest bidRequest, Account account, Imp correspondingImp, - BidderAliases aliases) throws ValidationException { + BidderAliases aliases, + BidRejectionTracker bidRejectionTracker) throws ValidationException { if (secureMarkupEnforcement == BidValidationEnforcement.skip) { return Collections.emptyList(); @@ -252,6 +275,7 @@ private List validateSecureMarkup(Bid bid, final String accountId = account.getId(); final String referer = getReferer(bidRequest); + final Bid bid = bidderBid.getBid(); final String adm = bid.getAdm(); if (isImpSecure(correspondingImp) && markupIsNotSecure(adm)) { @@ -264,7 +288,11 @@ private List validateSecureMarkup(Bid bid, secureMarkupEnforcement, metricName -> metrics.updateSecureValidationMetrics( aliases.resolveBidder(bidder), accountId, metricName), - SECURE_CREATIVE_LOGGER, message); + secureCreativeLogger, + message, + bidRejectionTracker, + bidderBid, + BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE); } return Collections.emptyList(); @@ -279,12 +307,18 @@ private static boolean markupIsNotSecure(String adm) { || !StringUtils.containsAny(adm, SECURE_MARKUP_MARKERS); } - private List singleWarningOrValidationException(BidValidationEnforcement enforcement, - Consumer metricsRecorder, - ConditionalLogger conditionalLogger, - String message) throws ValidationException { + private List singleWarningOrValidationException( + BidValidationEnforcement enforcement, + Consumer metricsRecorder, + ConditionalLogger conditionalLogger, + String message, + BidRejectionTracker bidRejectionTracker, + BidderBid bidderBid, + BidRejectionReason bidRejectionReason) throws ValidationException { + return switch (enforcement) { case enforce -> { + bidRejectionTracker.reject(BidRejection.of(bidderBid, bidRejectionReason)); metricsRecorder.accept(MetricName.err); conditionalLogger.warn(message, logSamplingRate); throw new ValidationException(message); @@ -302,165 +336,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..7158bd4ba07 --- /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.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..ea59c9f9670 --- /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.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..78a6a34ac7e --- /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.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/http/model/HttpClientResponse.java b/src/main/java/org/prebid/server/vertx/http/model/HttpClientResponse.java deleted file mode 100644 index 654d987d8b4..00000000000 --- a/src/main/java/org/prebid/server/vertx/http/model/HttpClientResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.vertx.http.model; - -import io.vertx.core.MultiMap; -import lombok.AllArgsConstructor; -import lombok.Value; - -/** - * Holds Http client response data. - *

- * Should be created in "bodyHandler(...) after response has been read." - */ -@AllArgsConstructor(staticName = "of") -@Value -public class HttpClientResponse { - - int statusCode; - - MultiMap headers; - - String body; -} 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/httpclient/model/HttpClientResponse.java b/src/main/java/org/prebid/server/vertx/httpclient/model/HttpClientResponse.java new file mode 100644 index 00000000000..af2f048672d --- /dev/null +++ b/src/main/java/org/prebid/server/vertx/httpclient/model/HttpClientResponse.java @@ -0,0 +1,19 @@ +package org.prebid.server.vertx.httpclient.model; + +import io.vertx.core.MultiMap; +import lombok.Value; + +/** + * Holds Http client response data. + *

+ * Should be created in "bodyHandler(...) after response has been read." + */ +@Value(staticConstructor = "of") +public class HttpClientResponse { + + int statusCode; + + MultiMap headers; + + String body; +} 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..55822047954 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,6 +6,7 @@ vertx: uploads-dir: file-uploads init-timeout-ms: 5000 enable-per-client-endpoint-metrics: false + round-robin-inet-address: false server: max-initial-line-length: 8092 max-headers-size: 16384 @@ -67,26 +68,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 +103,17 @@ 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 + profiles: + limit: 4 + timeout-ms: 100 timeout-notification: timeout-ms: 200 log-result: false @@ -145,7 +129,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 @@ -153,6 +137,7 @@ event: default-timeout-ms: 1000 setuid: default-timeout-ms: 2000 + number-of-uid-cookies: 1 vtrack: default-timeout-ms: 2000 allow-unknown-bidder: true @@ -178,9 +163,13 @@ currency-converter: settings: generate-storedrequest-bidrequest-id: false enforce-valid-account: false + fail-on-unknown-bidders: true + fail-on-disabled-bidders: true 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: > @@ -192,6 +181,7 @@ settings: "enabled": false, "timeout-ms": 5000, "max-rules": 0, + "max-schema-dims": 5, "max-file-size-kb": 200, "max-age-sec": 86400, "period-sec": 3600 @@ -199,7 +189,9 @@ settings: "enforce-floors-rate": 100, "adjust-for-bid-adjustment": true, "enforce-deal-floors": true, - "use-dynamic-data": true + "use-dynamic-data": true, + "max-rules": 100, + "max-schema-dims": 3 } } } @@ -301,6 +293,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 +305,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/adagio.yaml b/src/main/resources/bidder-config/adagio.yaml new file mode 100644 index 00000000000..ce66d58ce86 --- /dev/null +++ b/src/main/resources/bidder-config/adagio.yaml @@ -0,0 +1,28 @@ +adapters: + adagio: + # Please deploy this config in each of your datacenters with the appropriate regional subdomain. + # Replace the `REGION` by one of the value below: + # - For AMER: las => (https://mp-las.4dex.io/pbserver and https://u-las.4dex.io/pbserver/usync.html) + # - For EMEA: ams => (https://mp-ams.4dex.io/pbserver and https://u-ams.4dex.io/pbserver/usync.html) + # - For APAC: tyo => (https://mp-tyo.4dex.io/pbserver and https://u-tyo.4dex.io/pbserver/usync.html) + endpoint: https://mp-REGION.4dex.io/pbserver + ortb-version: "2.6" + endpoint-compression: gzip + meta-info: + maintainer-email: dev@adagio.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 617 + usersync: + cookie-family-name: adagio + iframe: + url: https://u-REGION.4dex.io/pbserver/usync.html?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/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..dbc1afb26a7 100644 --- a/src/main/resources/bidder-config/adkernel.yaml +++ b/src/main/resources/bidder-config/adkernel.yaml @@ -1,7 +1,24 @@ adapters: adkernel: - endpoint: https://pbs.adksrv.com/hb?zone=%s + endpoint: http://pbs.adksrv.com/hb?zone=%s endpoint-compression: gzip + aliases: + rxnetwork: ~ + 152media: ~ + xapads: + enabled: false + vendor-id: 1320 + usersync: + enabled: true + cookie-family-name: xapads + redirect: + url: https://sync.adkernel.com/user-sync?t=image&zone=284803&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}' + iframe: + url: https://sync.adkernel.com/user-sync?t=iframe&zone=284803&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}' meta-info: maintainer-email: prebid-dev@adkernel.com app-media-types: diff --git a/src/main/resources/bidder-config/admatic.yaml b/src/main/resources/bidder-config/admatic.yaml new file mode 100644 index 00000000000..9c0ac6a483e --- /dev/null +++ b/src/main/resources/bidder-config/admatic.yaml @@ -0,0 +1,38 @@ +adapters: + admatic: + endpoint: http://pbs.admatic.com.tr?host={{Host}} + aliases: + adt: + enabled: false + meta-info: + maintainer-email: publisher@adtarget.com.tr + vendor-id: 779 + pixad: + enabled: false + meta-info: + maintainer-email: prebid@pixad.com.tr + monetixads: + enabled: false + meta-info: + maintainer-email: team@monetixads.com + admaticde: + enabled: false + yobee: + enabled: false + meta-info: + maintainer-email: adops@yobee.it + netaddiction: + meta-info: + maintainer-email: publishers-support@netaddiction.it + 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/adnuntius.yaml b/src/main/resources/bidder-config/adnuntius.yaml index b90b743f81d..b8a3006ae24 100644 --- a/src/main/resources/bidder-config/adnuntius.yaml +++ b/src/main/resources/bidder-config/adnuntius.yaml @@ -1,11 +1,14 @@ adapters: adnuntius: endpoint: https://ads.adnuntius.delivery/i + eu-endpoint: https://europe.delivery.adnuntius.com/i meta-info: maintainer-email: hello@adnuntius.com app-media-types: - banner + - native site-media-types: - banner + - native supported-vendors: vendor-id: 855 diff --git a/src/main/resources/bidder-config/adoppler.yaml b/src/main/resources/bidder-config/adoppler.yaml deleted file mode 100644 index 1f66b131d83..00000000000 --- a/src/main/resources/bidder-config/adoppler.yaml +++ /dev/null @@ -1,13 +0,0 @@ -adapters: - adoppler: - endpoint: http://{{AccountID}}.trustedmarketplace.io/ads/processHeaderBid/{{AdUnit}} - meta-info: - maintainer-email: info@adoppler.com - app-media-types: - - banner - - video - site-media-types: - - banner - - video - supported-vendors: - vendor-id: 0 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/aduptech.yaml b/src/main/resources/bidder-config/aduptech.yaml new file mode 100644 index 00000000000..302051085bd --- /dev/null +++ b/src/main/resources/bidder-config/aduptech.yaml @@ -0,0 +1,25 @@ +adapters: + aduptech: + endpoint: https://rtb.d.adup-tech.com/rtb/bid + ortb-version: "2.6" + meta-info: + maintainer-email: support@adup-tech.com + app-media-types: + - banner + - native + site-media-types: + - banner + - native + supported-vendors: + vendor-id: 647 + usersync: + cookie-family-name: aduptech + iframe: + url: https://rtb.d.adup-tech.com/service/sync?iframe=1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + redirect: + url: https://rtb.d.adup-tech.com/service/sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + target-currency: "EUR" diff --git a/src/main/resources/bidder-config/adverxo.yaml b/src/main/resources/bidder-config/adverxo.yaml new file mode 100644 index 00000000000..d2fd18f505b --- /dev/null +++ b/src/main/resources/bidder-config/adverxo.yaml @@ -0,0 +1,70 @@ +adapters: + adverxo: + endpoint: https://pbsadverxo.com/auction?adUnitId={{adUnitId}}&auth={{auth}} + endpoint-compression: gzip + aliases: + adport: + enabled: false + endpoint: https://adport.pbsadverxo.com/auction?id={{adUnitId}}&auth={{auth}} + usersync: + enabled: false + cookie-family-name: adport + iframe: + url: https://cittamatra.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + redirect: + url: https://cittamatra.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + bidsmind: + enabled: false + endpoint: https://bidsmind.pbsadverxo.com/auction?id={{adUnitId}}&auth={{auth}} + usersync: + enabled: false + cookie-family-name: bidsmind + iframe: + url: https://taetee.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + redirect: + url: https://taetee.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + mobupps: + enabled: false + endpoint: https://mobupps.pbsadverxo.com/auction?id={{adUnitId}}&auth={{auth}} + usersync: + enabled: false + cookie-family-name: mobupps + iframe: + url: https://mobupps.pbsadverxo.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + redirect: + url: https://mobupps.pbsadverxo.com/usync?type=image&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + uid-macro: '$UID' + support-cors: false + meta-info: + maintainer-email: developer@adverxo.com + app-media-types: + - banner + - native + - video + site-media-types: + - banner + - native + - video + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: adverxo + iframe: + url: https://pbsadverxo.com/usync?type=iframe&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' + redirect: + url: https://pbsadverxo.com/usync?type=image&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/afront.yaml b/src/main/resources/bidder-config/afront.yaml new file mode 100644 index 00000000000..bd59185f20e --- /dev/null +++ b/src/main/resources/bidder-config/afront.yaml @@ -0,0 +1,22 @@ +# Contact support@afront.io to connect with Afront exchange. +# We have the following regional endpoint sub-domains: +# US East: snt1 +# APAC: snt2 +# EU: snt3 +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +adapters: + afront: + endpoint: https://snt1.afront.io/?rsd={{SourceId}}&sk={{AccountId}} + endpoint-compression: gzip + meta-info: + maintainer-email: support@afront.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/aidem.yaml b/src/main/resources/bidder-config/aidem.yaml index cd8b37239ab..5465aa6f558 100644 --- a/src/main/resources/bidder-config/aidem.yaml +++ b/src/main/resources/bidder-config/aidem.yaml @@ -1,17 +1,15 @@ 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: - banner - video - - native site-media-types: - banner - video - - native supported-vendors: vendor-id: 0 usersync: diff --git a/src/main/resources/bidder-config/akcelo.yaml b/src/main/resources/bidder-config/akcelo.yaml new file mode 100644 index 00000000000..dfec684d42d --- /dev/null +++ b/src/main/resources/bidder-config/akcelo.yaml @@ -0,0 +1,12 @@ +adapters: + akcelo: + endpoint: https://s2s.sportslocalmedia.com/openrtb2/auction + meta-info: + maintainer-email: tech@akcelo.io + app-media-types: + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml index 70c56825fdd..3fcc40e6361 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: @@ -8,5 +8,8 @@ adapters: - video - native site-media-types: + - banner + - video + - native supported-vendors: - vendor-id: 0 + vendor-id: 1176 diff --git a/src/main/resources/bidder-config/amx.yaml b/src/main/resources/bidder-config/amx.yaml index c4784662787..3dd46514e85 100644 --- a/src/main/resources/bidder-config/amx.yaml +++ b/src/main/resources/bidder-config/amx.yaml @@ -19,8 +19,8 @@ adapters: redirect: url: https://prebid.a-mo.net/cchain/0?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}} support-cors: false - userMacro: "$UID" + uid-macro: "$UID" iframe: url: https://prebid.a-mo.net/isyn?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&s=pbs&cb={{redirect_url}} - userMacro: "$UID" + uid-macro: "$UID" support-cors: false diff --git a/src/main/resources/bidder-config/aso.yaml b/src/main/resources/bidder-config/aso.yaml new file mode 100644 index 00000000000..fa6a6381741 --- /dev/null +++ b/src/main/resources/bidder-config/aso.yaml @@ -0,0 +1,30 @@ +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 + kuantyx: + enabled: false + endpoint: https://srv.kntxy.com/pbs/bidder?zid={{ZoneID}} + meta-info: + maintainer-email: ssp@kuantyx.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/bidtheatre.yaml b/src/main/resources/bidder-config/bidtheatre.yaml new file mode 100644 index 00000000000..fd3f7d24d87 --- /dev/null +++ b/src/main/resources/bidder-config/bidtheatre.yaml @@ -0,0 +1,20 @@ +adapters: + bidtheatre: + endpoint: https://client-bids.adsby.bidtheatre.com/prebidjsbid + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: operations@bidtheatre.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 30 + usersync: + cookie-family-name: bidtheatre + redirect: + url: https://match.adsby.bidtheatre.com/prebidmatch?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redir={{redirect_url}} + support-cors: false + uid-macro: '$UID' 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/blis.yaml b/src/main/resources/bidder-config/blis.yaml new file mode 100644 index 00000000000..6d9d50dad69 --- /dev/null +++ b/src/main/resources/bidder-config/blis.yaml @@ -0,0 +1,24 @@ +adapters: + blis: + endpoint: https://prebid.lb.infinity.blismedia.com/rtb/213/{{SupplyId}} + modifying-vast-xml-allowed: true + endpoint-compression: gzip + ortb-version: "2.6" + meta-info: + maintainer-email: prebid-support@blis.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 94 + usersync: + cookie-family-name: blis + redirect: + url: https://tr.blismedia.com/v1/api/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&r={{redirect_url}} + support-cors: false + uid-macro: '%%BLIS_USER_TOKEN%%' 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/boldwinrapid.yaml b/src/main/resources/bidder-config/boldwinrapid.yaml new file mode 100644 index 00000000000..f9b40571ea5 --- /dev/null +++ b/src/main/resources/bidder-config/boldwinrapid.yaml @@ -0,0 +1,11 @@ +adapters: + boldwin_rapid: + endpoint: https://rtb.beardfleet.com/auction/bid?pid={{PublisherID}}&tid={{PlacementID}} + meta-info: + maintainer-email: info@bold-win.com + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 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/colossus.yaml b/src/main/resources/bidder-config/colossus.yaml index 8e5bf6632d4..9514160b5a2 100644 --- a/src/main/resources/bidder-config/colossus.yaml +++ b/src/main/resources/bidder-config/colossus.yaml @@ -1,6 +1,7 @@ adapters: colossus: endpoint: http://colossusssp.com/?c=o&m=rtb + ortb-version: "2.6" aliases: colossusssp: enabled: false diff --git a/src/main/resources/bidder-config/compass.yaml b/src/main/resources/bidder-config/compass.yaml index 297cd97773f..954cd57eca4 100644 --- a/src/main/resources/bidder-config/compass.yaml +++ b/src/main/resources/bidder-config/compass.yaml @@ -16,6 +16,10 @@ adapters: usersync: cookie-family-name: compass redirect: - url: https://sa-cs.deliverimp.com/pserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://sa-cs.deliverimp.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}} support-cors: false uid-macro: '[UID]' + iframe: + support-cors: false + url: https://sa-cs.deliverimp.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}} + uid-macro: '[UID]' 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/connatix.yaml b/src/main/resources/bidder-config/connatix.yaml new file mode 100644 index 00000000000..63e1aca65c1 --- /dev/null +++ b/src/main/resources/bidder-config/connatix.yaml @@ -0,0 +1,24 @@ +adapters: + connatix: + endpoint: "https://capi.connatix.com/rtb/ortb" + endpoint-compression: gzip + meta-info: + maintainer-email: "pubsolutions@connatix.com" + vendor-id: 143 + app-media-types: + - banner + - video + site-media-types: + - banner + - video + usersync: + cookie-family-name: connatix + iframe: + url: "https://capi.connatix.com/us/pixel?pId=53&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&callback={{redirect_url}}" + uid-macro: '[UID]' + support-cors: false + redirect: + url: "https://capi.connatix.com/us/pixel?pId=52&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&callback={{redirect_url}}" + uid-macro: '[UID]' + support-cors: false + 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..49a528b41c9 100644 --- a/src/main/resources/bidder-config/consumable.yaml +++ b/src/main/resources/bidder-config/consumable.yaml @@ -1,12 +1,19 @@ adapters: consumable: - endpoint: https://e.serverbid.com/api/v2 + endpoint: https://e.serverbid.com + endpoint-compression: gzip meta-info: - maintainer-email: naffis@consumable.com + currency-accepted: + - USD + 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/contxtful.yaml b/src/main/resources/bidder-config/contxtful.yaml new file mode 100644 index 00000000000..086eb92c1dc --- /dev/null +++ b/src/main/resources/bidder-config/contxtful.yaml @@ -0,0 +1,21 @@ +adapters: + contxtful: + endpoint: https://prebid.receptivity.io/v1/pbs/{{AccountId}}/bid + meta-info: + maintainer-email: contact@contxtful.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: contxtful + iframe: + url: https://sync.receptivity.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: '$UID' diff --git a/src/main/resources/bidder-config/copper6ssp.yaml b/src/main/resources/bidder-config/copper6ssp.yaml new file mode 100644 index 00000000000..692697b6ea3 --- /dev/null +++ b/src/main/resources/bidder-config/copper6ssp.yaml @@ -0,0 +1,25 @@ +adapters: + copper6ssp: + endpoint: https://endpoint.copper6.com/pserver + meta-info: + maintainer-email: info@copper6.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1356 + 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..ba89ced1ec4 100644 --- a/src/main/resources/bidder-config/criteo.yaml +++ b/src/main/resources/bidder-config/criteo.yaml @@ -1,23 +1,31 @@ adapters: criteo: + ortb-version: "2.6" endpoint: https://ssp-bidder.criteo.com/openrtb/pbs/auction/request?profile=230 + geoscope: + - global + ortb: + multiformat-supported: true meta-info: maintainer-email: prebid@criteo.com 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/dianomi.yaml b/src/main/resources/bidder-config/dianomi.yaml index b0377810a72..6b36d1c29ab 100644 --- a/src/main/resources/bidder-config/dianomi.yaml +++ b/src/main/resources/bidder-config/dianomi.yaml @@ -16,10 +16,10 @@ adapters: usersync: cookie-family-name: dianomi iframe: - url: https://www-prebid.dianomi.com/prebid/usersync/index.html?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://www-prebid.dianomi.com/prebid/usersync/index.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false uid-macro: '$UID' redirect: - url: https://data.dianomi.com/frontend/usync?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + url: https://data.dianomi.com/frontend/usync?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/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..5bba4d9257f --- /dev/null +++ b/src/main/resources/bidder-config/driftpixel.yaml @@ -0,0 +1,21 @@ +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 + usersync: + cookie-family-name: driftpixel + redirect: + url: "https://sync.driftpixel.live/psync?t=s&e=0&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/elementaltv.yaml b/src/main/resources/bidder-config/elementaltv.yaml new file mode 100644 index 00000000000..198b887ea4b --- /dev/null +++ b/src/main/resources/bidder-config/elementaltv.yaml @@ -0,0 +1,18 @@ +adapters: + elementaltv: + endpoint: https://pbs.elementaltv.io/ads/processHeaderBid/{{AdUnit}} + ortb-version: "2.6" + aliases: + adoppler: ~ + meta-info: + maintainer-email: support@elementaltv.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/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..44970a25db2 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: @@ -10,9 +11,13 @@ adapters: app-media-types: - banner - video + - audio + - native site-media-types: - banner - video + - audio + - native supported-vendors: vendor-id: 24 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/exco.yaml b/src/main/resources/bidder-config/exco.yaml new file mode 100644 index 00000000000..df2a4dac0a1 --- /dev/null +++ b/src/main/resources/bidder-config/exco.yaml @@ -0,0 +1,19 @@ +adapters: + exco: + endpoint: https://v.ex.co/se/openrtb/hb/pbs + meta-info: + maintainer-email: itadmin@ex.co + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 444 + usersync: + cookie-family-name: exco + redirect: + url: https://sync.ex.co/v1/user_sync?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/feedad.yaml b/src/main/resources/bidder-config/feedad.yaml new file mode 100644 index 00000000000..fbd8f58627d --- /dev/null +++ b/src/main/resources/bidder-config/feedad.yaml @@ -0,0 +1,18 @@ +adapters: + feedad: + endpoint: https://ortb.feedad.com/1/prebid/requests + endpoint-compression: gzip + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: support@feedad.com + app-media-types: + - banner + site-media-types: + - banner + vendor-id: 781 + usersync: + cookie-family-name: feedad + iframe: + url: https://ortb.feedad.com/1/usersyncs/supply?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + support-cors: false + uid-macro: $UID diff --git a/src/main/resources/bidder-config/flatads.yaml b/src/main/resources/bidder-config/flatads.yaml new file mode 100644 index 00000000000..970d1c36b79 --- /dev/null +++ b/src/main/resources/bidder-config/flatads.yaml @@ -0,0 +1,17 @@ +adapters: + flatads: + endpoint: https://bid.rtbshark.com/api/rtbs/adx/rtb?x-net-id={{PublisherID}}&x-net-token={{TokenID}} + ortb-version: "2.6" + endpoint-compression: gzip + meta-info: + maintainer-email: adxbusiness@flat-ads.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/freewheelssp.yaml b/src/main/resources/bidder-config/freewheelssp.yaml index 1756a47cbd7..ff042fe76d2 100644 --- a/src/main/resources/bidder-config/freewheelssp.yaml +++ b/src/main/resources/bidder-config/freewheelssp.yaml @@ -1,7 +1,20 @@ adapters: freewheelssp: endpoint: https://ads.stickyadstv.com/openrtb/dsp - modifyingVastXmlAllowed: true + ortb-version: "2.6" + modifying-vast-xml-allowed: true + aliases: + fwssp: + enabled: false + endpoint: "https://prebid.v.fwmrm.net/ortb/ssp" + endpoint-compression: gzip + usersync: + enabled: true + cookie-family-name: fwssp + iframe: + url: https://user-sync.fwmrm.net/ad/u?mode=pbs-user-sync&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&pbs_redirect={{redirect_url}} + support-cors: false + uid-macro: '#{user.id}' 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..d4538733e05 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 @@ -23,10 +81,22 @@ adapters: meta-info: maintainer-email: devs@cwire.com app-media-types: + - banner site-media-types: - banner supported-vendors: vendor-id: 1081 + usersync: + enabled: true + cookie-family-name: cwire + redirect: + url: https://prebid.cwi.re/v1/usersync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&rd={{redirect_url}} + support-cors: false + uid-macro: '$UID' + iframe: + url: https://prebid.cwi.re/v1/usersync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&rd={{redirect_url}} + support-cors: false + uid-macro: '$UID' adsinteractive: enabled: false endpoint: http://bid.adsinteractive.com/prebid @@ -56,15 +126,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/gothamads.yaml b/src/main/resources/bidder-config/gothamads.yaml index bd1e2120945..eccdefb17e8 100644 --- a/src/main/resources/bidder-config/gothamads.yaml +++ b/src/main/resources/bidder-config/gothamads.yaml @@ -1,6 +1,12 @@ adapters: gothamads: endpoint: http://us-e-node1.gothamads.com/?pass={{AccountID}} + aliases: + intenze: + enabled: false + endpoint: http://lb-east.intenze.co/?pass={{AccountID}} + meta-info: + maintainer-email: connect@intenze.co meta-info: maintainer-email: support@gothamads.com app-media-types: 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/improvedigital.yaml b/src/main/resources/bidder-config/improvedigital.yaml index 4b7136b9a97..41af36899bf 100644 --- a/src/main/resources/bidder-config/improvedigital.yaml +++ b/src/main/resources/bidder-config/improvedigital.yaml @@ -1,6 +1,6 @@ adapters: improvedigital: - endpoint: http://ad.360yield.com/{{PathPrefix}}pbs + endpoint: https://ad.360yield.com/{{PathPrefix}}pbs endpoint-compression: gzip meta-info: maintainer-email: hb@azerion.com @@ -18,6 +18,10 @@ adapters: vendor-id: 253 usersync: cookie-family-name: improvedigital + iframe: + url: https://ad.360yield.com/user_sync?rt=html&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} + support-cors: false + uid-macro: '{PUB_USER_ID}' redirect: url: https://ad.360yield.com/server_match?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false 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..fb5ddb9c7ee 100644 --- a/src/main/resources/bidder-config/inmobi.yaml +++ b/src/main/resources/bidder-config/inmobi.yaml @@ -10,11 +10,16 @@ adapters: site-media-types: - banner - video + - native supported-vendors: vendor-id: 333 usersync: cookie-family-name: inmobi + 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}' redirect: - url: https://sync.inmobi.com/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} + 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/insticator.yaml b/src/main/resources/bidder-config/insticator.yaml new file mode 100644 index 00000000000..7fcf60b877c --- /dev/null +++ b/src/main/resources/bidder-config/insticator.yaml @@ -0,0 +1,19 @@ +adapters: + insticator: + endpoint: https://ex.ingage.tech/v1/prebidserver + meta-info: + maintainer-email: prebid@insticator.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 910 + usersync: + cookie-family-name: insticator + iframe: + url: https://usync.ingage.tech?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/iqzone.yaml b/src/main/resources/bidder-config/iqzone.yaml index ccd2f126f3f..a8292c5033a 100644 --- a/src/main/resources/bidder-config/iqzone.yaml +++ b/src/main/resources/bidder-config/iqzone.yaml @@ -13,3 +13,13 @@ adapters: - native supported-vendors: vendor-id: 0 + usersync: + cookie-family-name: iqzone + redirect: + url: https://cs.iqzone.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.iqzone.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/ix.yaml b/src/main/resources/bidder-config/ix.yaml index 0a5c8819df9..725c2494701 100644 --- a/src/main/resources/bidder-config/ix.yaml +++ b/src/main/resources/bidder-config/ix.yaml @@ -1,6 +1,8 @@ adapters: ix: + # Please contact the maintainer to obtain endpoint details 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/kobler.yaml b/src/main/resources/bidder-config/kobler.yaml new file mode 100644 index 00000000000..cd49ef2ea75 --- /dev/null +++ b/src/main/resources/bidder-config/kobler.yaml @@ -0,0 +1,16 @@ +adapters: + kobler: + endpoint: "https://bid.essrtb.com/bid/prebid_server_rtb_call" + dev-endpoint: "https://bid-service.dev.essrtb.com/bid/prebid_server_rtb_call" + endpoint-compression: gzip + geoscope: + - NOR + - SWE + - DNK + meta-info: + maintainer-email: bidding-support@kobler.no + site-media-types: + - banner + app-media-types: + - banner + vendor-id: 0 diff --git a/src/main/resources/bidder-config/krushmedia.yaml b/src/main/resources/bidder-config/krushmedia.yaml index 46b2d2aa47e..0f2a42e7785 100644 --- a/src/main/resources/bidder-config/krushmedia.yaml +++ b/src/main/resources/bidder-config/krushmedia.yaml @@ -16,6 +16,10 @@ adapters: usersync: cookie-family-name: krushmedia redirect: - url: https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}} + url: https://cs.krushmedia.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]' + uid-macro: '[UID]' + iframe: + url: https://cs.krushmedia.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/kueezrtb.yaml b/src/main/resources/bidder-config/kueezrtb.yaml new file mode 100644 index 00000000000..b26c7ad4ea8 --- /dev/null +++ b/src/main/resources/bidder-config/kueezrtb.yaml @@ -0,0 +1,20 @@ +adapters: + kueezrtb: + endpoint: https://prebidsrvr.kueezrtb.com/openrtb/ + endpoint-compression: gzip + meta-info: + maintainer-email: rtb@kueez.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 1165 + usersync: + cookie-family-name: kueezrtb + iframe: + url: https://sync.kueezrtb.com/api/user/html/62ce79e7dd15099534ae5e04?pbs=true&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/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..961d909e0d0 100644 --- a/src/main/resources/bidder-config/limelightDigital.yaml +++ b/src/main/resources/bidder-config/limelightDigital.yaml @@ -2,9 +2,13 @@ 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}} + meta-info: + vendor-id: 1358 evtech: enabled: false endpoint: http://ads-pbs.direct.e-volution.ai/openrtb/{{PublisherID}}?host={{Host}} @@ -34,6 +38,26 @@ 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}} + streamvision: + enabled: false + endpoint: http://ads-pbs.adops.streamvisionmedia.com/openrtb/{{PublisherID}}?host={{Host}} + orangeclickmedia: + enabled: false + endpoint: http://ads-pbs.scotty.orangeclickmedia.com/openrtb/{{PublisherID}}?host={{Host}} + velonium: + enabled: false + endpoint: http://ads-pbs.adxvel.com/openrtb/{{PublisherID}}?host={{Host}} + adtg_org: + enabled: false + endpoint: http://ads-pbs.rtb.adtarget.org/openrtb/{{PublisherID}}?host={{Host}} + performist: + enabled: false + endpoint: http://ads-pbs.performserv.com/openrtb/{{PublisherID}} 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/madsense.yaml b/src/main/resources/bidder-config/madsense.yaml new file mode 100644 index 00000000000..29dc214b093 --- /dev/null +++ b/src/main/resources/bidder-config/madsense.yaml @@ -0,0 +1,13 @@ +adapters: + madsense: + endpoint: https://ads.madsense.io/pbs + meta-info: + maintainer-email: prebid@madsense.io + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 0 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/medianet.yaml b/src/main/resources/bidder-config/medianet.yaml index 63aecb5f2b0..cf4119e4147 100644 --- a/src/main/resources/bidder-config/medianet.yaml +++ b/src/main/resources/bidder-config/medianet.yaml @@ -17,6 +17,10 @@ adapters: vendor-id: 142 usersync: cookie-family-name: medianet + iframe: + url: https://hbx.media.net/checksync.php?cid=8CUEHS6F9&cs=87&type=mpbc&cv=37&vsSync=1&uspstring={{us_privacy}}&gdpr={{gdpr}}&gdprstring={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '' redirect: url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} support-cors: false diff --git a/src/main/resources/bidder-config/mediasquare.yaml b/src/main/resources/bidder-config/mediasquare.yaml new file mode 100644 index 00000000000..b8be22071d8 --- /dev/null +++ b/src/main/resources/bidder-config/mediasquare.yaml @@ -0,0 +1,13 @@ +adapters: + mediasquare: + endpoint: "https://pbs-front.mediasquare.fr/msq_prebid" + endpoint-compression: gzip + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: tech@mediasquare.fr + app-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 791 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..493e0640bfd --- /dev/null +++ b/src/main/resources/bidder-config/metax.yaml @@ -0,0 +1,24 @@ +# 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 + usersync: + cookie-family-name: metax + redirect: + url: https://cm.metaxads.com/pixel?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{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/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/minutemedia.yaml b/src/main/resources/bidder-config/minutemedia.yaml index 5271b51be67..096f7776ded 100644 --- a/src/main/resources/bidder-config/minutemedia.yaml +++ b/src/main/resources/bidder-config/minutemedia.yaml @@ -1,6 +1,7 @@ adapters: minutemedia: endpoint: https://pbs.minutemedia-prebid.com/pbs-mm?publisher_id={{PublisherId}} + test-endpoint: https://pbs.minutemedia-prebid.com/pbs-test?publisher_id={{PublisherId}} modifying-vast-xml-allowed: true meta-info: maintainer-email: hb@minutemedia.com diff --git a/src/main/resources/bidder-config/missena.yaml b/src/main/resources/bidder-config/missena.yaml new file mode 100644 index 00000000000..f8921eac3db --- /dev/null +++ b/src/main/resources/bidder-config/missena.yaml @@ -0,0 +1,18 @@ +adapters: + missena: + endpoint: https://bid.missena.io/?t={{PublisherID}} + 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..0942ce43ef9 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= + endpoint: http://mfx.mobilefuse.com/openrtb + ortb-version: "2.6" endpoint-compression: gzip # This bidder does not operate globally. Please consider setting "disabled: true" outside of the following regions: geoscope: @@ -15,3 +16,13 @@ adapters: site-media-types: supported-vendors: vendor-id: 909 + usersync: + cookie-family-name: mobilefuse + iframe: + url: https://mfx.mobilefuse.com/usync?us_privacy={{us_privacy}}&pxurl={{redirect_url}} + support-cors: false + uid-macro: '$UID' + redirect: + url: https://mfx.mobilefuse.com/getuid?us_privacy={{us_privacy}}&redir={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/mobkoi.yaml b/src/main/resources/bidder-config/mobkoi.yaml new file mode 100644 index 00000000000..154d629c42b --- /dev/null +++ b/src/main/resources/bidder-config/mobkoi.yaml @@ -0,0 +1,11 @@ +adapters: + mobkoi: + endpoint: "https://pbs.mobkoi.com/bid/prebidserver" + ortb-version: "2.6" + meta-info: + maintainer-email: platformteam@mobkoi.com + app-media-types: + site-media-types: + - banner + supported-vendors: + vendor-id: 898 diff --git a/src/main/resources/bidder-config/nativery.yaml b/src/main/resources/bidder-config/nativery.yaml new file mode 100644 index 00000000000..e37bc8dcd4d --- /dev/null +++ b/src/main/resources/bidder-config/nativery.yaml @@ -0,0 +1,12 @@ +adapters: + nativery: + endpoint: "https://hb.nativery.com/openrtb2/auction" + ortb-version: "2.6" + meta-info: + maintainer-email: "developer@nativery.com" + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1133 diff --git a/src/main/resources/bidder-config/nextmillennium.yaml b/src/main/resources/bidder-config/nextmillennium.yaml index 51524341a45..67c34ae62d4 100644 --- a/src/main/resources/bidder-config/nextmillennium.yaml +++ b/src/main/resources/bidder-config/nextmillennium.yaml @@ -1,12 +1,15 @@ adapters: nextmillennium: endpoint: https://pbs.nextmillmedia.com/openrtb2/auction + endpoint-compression: gzip meta-info: maintainer-email: accountmanagers@nextmillennium.io app-media-types: - banner + - video site-media-types: - banner + - video supported-vendors: vendor-id: 1060 usersync: @@ -18,4 +21,4 @@ adapters: redirect: url: https://cookies.nextmillmedia.com/sync?type=image&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}} support-cors: false - userMacro: '[NMUID]' + uid-macro: '[NMUID]' diff --git a/src/main/resources/bidder-config/nexx360.yaml b/src/main/resources/bidder-config/nexx360.yaml new file mode 100644 index 00000000000..443ca7f5ab4 --- /dev/null +++ b/src/main/resources/bidder-config/nexx360.yaml @@ -0,0 +1,22 @@ +adapters: + nexx360: + endpoint: http://fast.nexx360.io/prebid-server + endpoint-compression: gzip + aliases: + 1accord: ~ + easybid: ~ + prismassp: ~ + meta-info: + maintainer-email: tech@nexx360.io + app-media-types: + - banner + - video + - native + - audio + site-media-types: + - banner + - video + - native + - audio + supported-vendors: + vendor-id: 0 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/ogury.yaml b/src/main/resources/bidder-config/ogury.yaml new file mode 100644 index 00000000000..b1172241a25 --- /dev/null +++ b/src/main/resources/bidder-config/ogury.yaml @@ -0,0 +1,23 @@ +adapters: + ogury: + endpoint: "https://prebids2s.presage.io/api/header-bidding-request" + endpointCompression: gzip + geoscope: + - global + meta-info: + maintainer-email: deliveryservices@ogury.co + site-media-types: + - banner + app-media-types: + - banner + vendor-id: 31 + usersync: + cookie-family-name: ogury + iframe: + url: https://ms-cookie-sync.presage.io/user-sync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&source=prebids2s + uid-macro: "{{OGURY_UID}}" + support-cors: false + redirect: + url: https://ms-cookie-sync.presage.io/user-sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}&us_privacy={{us_privacy}}&redirect={{redirect_url}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&partner=prebids2s + uid-macro: "{{OGURY_UID}}" + support-cors: false diff --git a/src/main/resources/bidder-config/oms.yaml b/src/main/resources/bidder-config/oms.yaml index 0b46d3e01ed..16c4a4cac08 100644 --- a/src/main/resources/bidder-config/oms.yaml +++ b/src/main/resources/bidder-config/oms.yaml @@ -5,7 +5,9 @@ adapters: maintainer-email: prebid@onlinemediasolutions.com app-media-types: - banner + - video site-media-types: - banner + - video supported-vendors: vendor-id: 0 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..2a5002667e0 100644 --- a/src/main/resources/bidder-config/openx.yaml +++ b/src/main/resources/bidder-config/openx.yaml @@ -1,22 +1,26 @@ 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: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 69 usersync: cookie-family-name: openx iframe: - url: https://u.openx.net/w/1.0/cm?id=891039ac-a916-42bb-a651-4be9e3b201da&ph=a3aece0c-9e80-4316-8deb-faf804779bd1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r={{redirect_url}} + url: https://u.openx.net/w/1.0/cm?id=891039ac-a916-42bb-a651-4be9e3b201da&ph=a3aece0c-9e80-4316-8deb-faf804779bd1&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false redirect: - url: https://rtb.openx.net/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&r={{redirect_url}} + url: https://rtb.openx.net/sync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&r={{redirect_url}} support-cors: false uid-macro: '${UID}' diff --git a/src/main/resources/bidder-config/optidigital.yaml b/src/main/resources/bidder-config/optidigital.yaml new file mode 100644 index 00000000000..756e0d71b2e --- /dev/null +++ b/src/main/resources/bidder-config/optidigital.yaml @@ -0,0 +1,22 @@ +adapters: + optidigital: + enabled: false + endpoint: https://pbs.optidigital.com/bidder/openrtb2 + endpoint-compression: gzip + ortb-version: "2.6" + meta-info: + maintainer-email: prebid@optidigital.com + app-media-types: + - banner + site-media-types: + - banner + dooh-media-types: + - banner + supported-vendors: + vendor-id: 915 + usersync: + cookie-family-name: optidigital + iframe: + url: https://scripts.opti-digital.com/js/presyncs2s.html?endpoint=optidigital&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{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/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..70105cab774 100644 --- a/src/main/resources/bidder-config/pgamssp.yaml +++ b/src/main/resources/bidder-config/pgamssp.yaml @@ -12,10 +12,10 @@ adapters: - video - native supported-vendors: - vendor-id: 0 + vendor-id: 1353 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/pubmatic.yaml b/src/main/resources/bidder-config/pubmatic.yaml index 8f59f1912f1..81f9b89f6a1 100644 --- a/src/main/resources/bidder-config/pubmatic.yaml +++ b/src/main/resources/bidder-config/pubmatic.yaml @@ -1,6 +1,8 @@ adapters: pubmatic: endpoint: https://hbopenbid.pubmatic.com/translator?source=prebid-server + endpoint-compression: gzip + ortb-version: "2.6" meta-info: maintainer-email: header-bidding@pubmatic.com app-media-types: diff --git a/src/main/resources/bidder-config/pubrise.yaml b/src/main/resources/bidder-config/pubrise.yaml new file mode 100644 index 00000000000..91fb0e673a8 --- /dev/null +++ b/src/main/resources/bidder-config/pubrise.yaml @@ -0,0 +1,25 @@ +adapters: + pubrise: + endpoint: https://backend.pubrise.ai/pserver + 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/rediads.yaml b/src/main/resources/bidder-config/rediads.yaml new file mode 100644 index 00000000000..c885129acba --- /dev/null +++ b/src/main/resources/bidder-config/rediads.yaml @@ -0,0 +1,20 @@ +adapters: + rediads: + endpoint: https://{{SUBDOMAIN}}.rediads.com/openrtb2/auction + default-subdomain: bidding + ortb-version: "2.6" + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: support@rediads.com + app-media-types: + - banner + - video + - audio + - native + site-media-types: + - banner + - video + - audio + - native + supported-vendors: + vendor-id: 0 diff --git a/src/main/resources/bidder-config/richaudience.yaml b/src/main/resources/bidder-config/richaudience.yaml index ebe84aec464..4ee2e2ef453 100644 --- a/src/main/resources/bidder-config/richaudience.yaml +++ b/src/main/resources/bidder-config/richaudience.yaml @@ -1,6 +1,6 @@ adapters: richaudience: - endpoint: http://ortb.richaudience.com/ortb/?bidder=pbs + endpoint: https://ortb.richaudience.com/ortb/?bidder=pbs meta-info: maintainer-email: partnerintegrations@richaudience.com app-media-types: @@ -17,3 +17,7 @@ adapters: url: https://sync.richaudience.com/74889303289e27f327ad0c6de7be7264/?consentString={{gdpr_consent}}&r={{redirect_url}} support-cors: false uid-macro: '[PDID]' + redirect: + url: https://sync.richaudience.com/f7872c90c5d3791e2b51f7edce1a0a5d/?p=pbs&consentString={{gdpr_consent}}&r={{redirect_url}} + support-cors: false + uid-macro: '[PDID]' diff --git a/src/main/resources/bidder-config/rise.yaml b/src/main/resources/bidder-config/rise.yaml index f85876dd998..f8244da16f8 100644 --- a/src/main/resources/bidder-config/rise.yaml +++ b/src/main/resources/bidder-config/rise.yaml @@ -2,19 +2,22 @@ adapters: rise: endpoint: https://pbs.yellowblue.io/pbs modifying-vast-xml-allowed: true + endpoint-compression: gzip meta-info: maintainer-email: rise-prog-dev@risecodes.com app-media-types: - banner - video + - native site-media-types: - banner - video + - native supported-vendors: vendor-id: 1043 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..55f6eda1a6b 100644 --- a/src/main/resources/bidder-config/rtbhouse.yaml +++ b/src/main/resources/bidder-config/rtbhouse.yaml @@ -1,15 +1,29 @@ 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 + ortb-version: "2.6" 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..8092766796c 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: @@ -35,7 +37,7 @@ adapters: redirect: url: GET_FROM_globalsupport@magnite.com support-cors: false - generate-bid-id: false + apex-renderer-url: "https://video-outstream.rubiconproject.com/apex-2.2.1.js" XAPI: Username: GET_FROM_globalsupport@magnite.com Password: GET_FROM_globalsupport@magnite.com diff --git a/src/main/resources/bidder-config/seedtag.yaml b/src/main/resources/bidder-config/seedtag.yaml new file mode 100644 index 00000000000..44eed6fd01b --- /dev/null +++ b/src/main/resources/bidder-config/seedtag.yaml @@ -0,0 +1,17 @@ +adapters: + seedtag: + endpoint: "https://s.seedtag.com/c/openrtb?partner=prebidserver" + endpoint-compression: gzip + meta-info: + maintainer-email: prebid@seedtag.com + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 157 + usersync: + cookie-family-name: seedtag + iframe: + url: https://s.seedtag.com/cs/cookiesync/prebid?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&usp_consent={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml index d108840dd26..5bf1dd4b2ed 100644 --- a/src/main/resources/bidder-config/sharethrough.yaml +++ b/src/main/resources/bidder-config/sharethrough.yaml @@ -1,6 +1,9 @@ adapters: sharethrough: endpoint: https://btlr.sharethrough.com/universal/v1?supply_id=FGMrCMMc + ortb-version: '2.6' + geoscope: + - global meta-info: maintainer-email: pubgrowth.engineering@sharethrough.com app-media-types: @@ -16,6 +19,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/showheroes.yaml b/src/main/resources/bidder-config/showheroes.yaml new file mode 100644 index 00000000000..b5fd953ac71 --- /dev/null +++ b/src/main/resources/bidder-config/showheroes.yaml @@ -0,0 +1,17 @@ +adapters: + showheroes: + endpoint: https://ads.viralize.tv/openrtb2/auction/ + aliases: + showheroes-bs: ~ + showheroesBs: ~ + ortb-version: '2.6' + meta-info: + maintainer-email: tech@showheroes.com + app-media-types: + - banner + - video + site-media-types: + - banner + - video + supported-vendors: + vendor-id: 111 diff --git a/src/main/resources/bidder-config/smaato.yaml b/src/main/resources/bidder-config/smaato.yaml index 58d5ddd4a17..d1f93997dc0 100644 --- a/src/main/resources/bidder-config/smaato.yaml +++ b/src/main/resources/bidder-config/smaato.yaml @@ -2,14 +2,18 @@ adapters: smaato: endpoint: https://prebid.ad.smaato.net/oapi/prebid endpoint-compression: gzip + geoscope: + - global meta-info: maintainer-email: prebid@smaato.com 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 @@ -20,3 +24,7 @@ adapters: url: https://s.ad.smaato.net/c/?adExInit=p&redir={{redirect_url}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}} support-cors: false uid-macro: '$UID' + iframe: + url: https://s.ad.smaato.net/i/?adExInit=p&redir={{redirect_url}}&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}} + support-cors: false + uid-macro: '$UID' diff --git a/src/main/resources/bidder-config/smartadserver.yaml b/src/main/resources/bidder-config/smartadserver.yaml index a4ff6fff510..343bec392de 100644 --- a/src/main/resources/bidder-config/smartadserver.yaml +++ b/src/main/resources/bidder-config/smartadserver.yaml @@ -1,16 +1,23 @@ adapters: smartadserver: endpoint: https://ssb-global.smartadserver.com + secondary-endpoint: https://prebid-global.smartadserver.com + endpoint-compression: gzip + aliases: + equativ: + enabled: false meta-info: maintainer-email: supply-partner-integration@equativ.com app-media-types: - banner - video - native + - audio site-media-types: - banner - video - native + - audio supported-vendors: vendor-id: 45 usersync: diff --git a/src/main/resources/bidder-config/smarthub.yaml b/src/main/resources/bidder-config/smarthub.yaml index 8b1d8de2869..73a50212676 100644 --- a/src/main/resources/bidder-config/smarthub.yaml +++ b/src/main/resources/bidder-config/smarthub.yaml @@ -1,8 +1,36 @@ adapters: smarthub: - endpoint: http://{{Host}}-prebid.smart-hub.io/?seat={{AccountId}}&token={{SourceId}} + endpoint: https://prebid.attekmi.com/pbserver?partnerName={{Host}}&seat={{AccountID}}&token={{SourceId}} + aliases: + markapp: + enabled: false + endpoint: https://markapp-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + jdpmedia: + enabled: false + endpoint: https://jdpmedia-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + tredio: + enabled: false + endpoint: https://tredio-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + vimayx: + enabled: false + endpoint: https://vimayx-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + felixads: + enabled: false + endpoint: https://felixads-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + jambojar: + enabled: false + endpoint: https://jambojar-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + adinify: + enabled: false + endpoint: https://adinify-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + artechnology: + enabled: false + endpoint: https://artechnology-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} + addigi: + enabled: false + endpoint: https://addigi-prebid.attekmi.com/pbserver/?seat={{AccountID}}&token={{SourceId}} meta-info: - maintainer-email: support@smart-hub.io + maintainer-email: prebid@attekmi.com app-media-types: - banner - video 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/smilewanted.yaml b/src/main/resources/bidder-config/smilewanted.yaml index 72e9218efe8..3cac81ccae3 100644 --- a/src/main/resources/bidder-config/smilewanted.yaml +++ b/src/main/resources/bidder-config/smilewanted.yaml @@ -1,6 +1,6 @@ adapters: smilewanted: - endpoint: http://prebid-server.smilewanted.com + endpoint: https://prebid-server.smilewanted.com/java/{{ZoneId}} meta-info: maintainer-email: tech@smilewanted.com app-media-types: diff --git a/src/main/resources/bidder-config/smoot.yaml b/src/main/resources/bidder-config/smoot.yaml new file mode 100644 index 00000000000..a1690cbb8ba --- /dev/null +++ b/src/main/resources/bidder-config/smoot.yaml @@ -0,0 +1,21 @@ +adapters: + smoot: + endpoint: 'https://endpoint1.smoot.ai/pserver' + meta-info: + maintainer-email: 'info@smoot.ai' + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 0 + usersync: + cookie-family-name: smoot + redirect: + support-cors: false + url: 'https://usync.smxconv.com/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/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/sparteo.yaml b/src/main/resources/bidder-config/sparteo.yaml new file mode 100644 index 00000000000..5dd9dd19b1d --- /dev/null +++ b/src/main/resources/bidder-config/sparteo.yaml @@ -0,0 +1,20 @@ +adapters: + sparteo: + endpoint: https://bid.sparteo.com/s2s-auction + meta-info: + maintainer-email: prebid@sparteo.com + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + supported-vendors: + vendor-id: 1028 + usersync: + cookie-family-name: sparteo + iframe: + url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&&gpp_sid={{gpp_sid}}&redirect_url={{redirect_url}}" + support-cors: true diff --git a/src/main/resources/bidder-config/sspbc.yaml b/src/main/resources/bidder-config/sspbc.yaml index 474e0709d7e..e81c64f7069 100644 --- a/src/main/resources/bidder-config/sspbc.yaml +++ b/src/main/resources/bidder-config/sspbc.yaml @@ -1,6 +1,6 @@ adapters: sspbc: - endpoint: https://ssp.wp.pl/bidder/ + endpoint: https://ssp.wp.pl/v2/bidder/prebidserver meta-info: maintainer-email: prebid-dev@grupawp.pl app-media-types: @@ -14,3 +14,7 @@ adapters: url: https://ssp.wp.pl/bidder/usersync?tcf=2&redirect={{redirect_url}} support-cors: false uid-macro: '$UID' + redirect: + url: https://ssp.wp.pl/v1/sync/prebid-server/pixel?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '$UID' \ No newline at end of file diff --git a/src/main/resources/bidder-config/startio.yaml b/src/main/resources/bidder-config/startio.yaml new file mode 100644 index 00000000000..50be8e27fc2 --- /dev/null +++ b/src/main/resources/bidder-config/startio.yaml @@ -0,0 +1,14 @@ +adapters: + startio: + endpoint: http://pbs-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbs + meta-info: + maintainer-email: prebid@start.io + app-media-types: + - banner + - video + - native + site-media-types: + - banner + - video + - native + vendor-id: 1216 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/teqblaze.yaml b/src/main/resources/bidder-config/teqblaze.yaml new file mode 100644 index 00000000000..5e1006433ae --- /dev/null +++ b/src/main/resources/bidder-config/teqblaze.yaml @@ -0,0 +1,57 @@ +adapters: + teqblaze: + endpoint: http:// + aliases: + pinkLion: + enabled: false + endpoint: https://us-east-ep.pinklion.io/pserver + meta-info: + maintainer-email: prebid@pinklion.io + rocketlab: + enabled: false + endpoint: https://traffic1.rocketlab.ai/pserver + meta-info: + maintainer-email: support@rocketlab.ai + usersync: + enabled: true + cookie-family-name: rocketlab + redirect: + url: https://usync.rocketlab.ai/pbserver?gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '[UID]' + appStockSSP: + enabled: false + # We have the following regional endpoint domains: 'lb' - for US_EAST, 'ortb-eu' - for EU, 'ortb-apac' - for APAC + # Please deploy this config in each of your datacenters with the appropriate regional subdomain + endpoint: https://#{REGION}#.al-ad.com/pserver + meta-info: + maintainer-email: sdksupport@app-stock.com + usersync: + enabled: true + cookie-family-name: appStockSSP + redirect: + url: https://csync.al-ad.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://csync.al-ad.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]' + gravite: + enabled: false + endpoint: https://node1-rtb.gravite.net/pserver + mata-info: + maintainer-email: support@gravite.net + vendor-id: 377 + meta-info: + maintainer-email: github@teqblaze.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/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..e8b9976e786 --- /dev/null +++ b/src/main/resources/bidder-config/thetradedesk.yaml @@ -0,0 +1,17 @@ +adapters: + thetradedesk: + endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}} + aliases: + ttd: ~ + 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/thirtythreeacross.yaml b/src/main/resources/bidder-config/thirtythreeacross.yaml index 3a7f1dd692d..98630182983 100644 --- a/src/main/resources/bidder-config/thirtythreeacross.yaml +++ b/src/main/resources/bidder-config/thirtythreeacross.yaml @@ -20,6 +20,6 @@ adapters: usersync: cookie-family-name: 33across iframe: - url: https://ssc-cms.33across.com/ps/?m=xch&rt=html&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&id=zzz000000000002zzz&ru={{redirect_url}} + url: https://ssc-cms.33across.com/ps/?m=xch&rt=html&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&id=zzz000000000002zzz&ru={{redirect_url}} support-cors: false uid-macro: '33XUSERID33X' diff --git a/src/main/resources/bidder-config/tpmn.yaml b/src/main/resources/bidder-config/tpmn.yaml index 4bfe0d61cbc..35041d577a2 100644 --- a/src/main/resources/bidder-config/tpmn.yaml +++ b/src/main/resources/bidder-config/tpmn.yaml @@ -19,5 +19,5 @@ adapters: cookie-family-name: tpmn iframe: url: https://gat.tpmn.io/sync/redirect?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}} - userMacro: $UID + uid-macro: $UID support-cors: false 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..3685e3e93a2 --- /dev/null +++ b/src/main/resources/bidder-config/trustedstack.yaml @@ -0,0 +1,27 @@ +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 + iframe: + url: https://hb.trustedstack.com/checksync.php?cid=TS2Q14L8J&cs=87&type=mpbc&cv=37&vsSync=1&uspstring={{us_privacy}}&gdpr={{gdpr}}&gdprstring={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}} + support-cors: false + uid-macro: '' + 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/undertone.yaml b/src/main/resources/bidder-config/undertone.yaml index 67ecd50901e..e18f3afa6f8 100644 --- a/src/main/resources/bidder-config/undertone.yaml +++ b/src/main/resources/bidder-config/undertone.yaml @@ -2,6 +2,7 @@ adapters: undertone: endpoint: https://ads.undertone.com/rtb endpoint-compression: gzip + ortb-version: "2.6" meta-info: maintainer-email: csop@undertone.com app-media-types: diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml index 050c61c62d9..0b239b0a3f7 100644 --- a/src/main/resources/bidder-config/unruly.yaml +++ b/src/main/resources/bidder-config/unruly.yaml @@ -1,6 +1,7 @@ adapters: unruly: endpoint: https://targeting.unrulymedia.com/unruly_prebid_server + ortb-version: "2.6" 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..3e1768fb0cb --- /dev/null +++ b/src/main/resources/bidder-config/vidazoo.yaml @@ -0,0 +1,56 @@ +adapters: + vidazoo: + endpoint: https://prebidsrvr.cootlogix.com/openrtb/ + aliases: + progx: + enabled: false + endpoint: https://exchange.programmaticx.ai/openrtb/ + meta-info: + maintainer-email: pxteam@programmaticx.ai + vendor-id: 1344 + usersync: + enabled: true + cookie-family-name: progx + iframe: + url: https://sync.programmaticx.ai/api/user/html/685297194d85991a5e6e36dd?pbs=true&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}' + omnidex: + endpoint: https://exchange.omni-dex.io/openrtb/ + usersync: + enabled: true + cookie-family-name: omnidex + vendor-id: 1463 + iframe: + url: https://sync.omni-dex.io/api/user/html/6810d0c7f163277130f3d7a9?pbs=true&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}' + tagoras: + endpoint: https://exchange.tagoras.io/openrtb/ + meta-info: + maintainer-email: prebid@tagoras.io + usersync: + enabled: true + cookie-family-name: tagoras + iframe: + url: https://sync.tagoras.io/api/user/html/6819bdc3e6bb44545c55f843?pbs=true&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}' + endpoint-compression: gzip + ortb-version: "2.6" + 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/visx.yaml b/src/main/resources/bidder-config/visx.yaml index 9d99f8a31f0..df8384fc6c1 100644 --- a/src/main/resources/bidder-config/visx.yaml +++ b/src/main/resources/bidder-config/visx.yaml @@ -1,6 +1,6 @@ adapters: visx: - endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_java + endpoint: https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard:0.1.2 meta-info: maintainer-email: supply.partners@yoc.com app-media-types: diff --git a/src/main/resources/bidder-config/vox.yaml b/src/main/resources/bidder-config/vox.yaml index bae41cd24c4..503653d64bd 100644 --- a/src/main/resources/bidder-config/vox.yaml +++ b/src/main/resources/bidder-config/vox.yaml @@ -17,5 +17,5 @@ adapters: cookie-family-name: vox iframe: url: https://ssp.hybrid.ai/prebid/server/v1/userSync?consent={{gdpr_consent}}&redirect={{redirect_url}} - userMacro: $UID + uid-macro: $UID support-cors: false diff --git a/src/main/resources/bidder-config/vrtcal.yaml b/src/main/resources/bidder-config/vrtcal.yaml index 883488a1a4d..1373df9e4f6 100644 --- a/src/main/resources/bidder-config/vrtcal.yaml +++ b/src/main/resources/bidder-config/vrtcal.yaml @@ -1,6 +1,7 @@ adapters: vrtcal: endpoint: http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1804 + ortb-version: "2.6" meta-info: maintainer-email: support@vrtcal.com app-media-types: 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/xeworks.yaml b/src/main/resources/bidder-config/xeworks.yaml index 339a40178ce..6464a99c8ae 100644 --- a/src/main/resources/bidder-config/xeworks.yaml +++ b/src/main/resources/bidder-config/xeworks.yaml @@ -1,6 +1,28 @@ adapters: xeworks: endpoint: http://prebid-srv.xe.works/?pid={{SourceId}}&host={{Host}} + aliases: + connektai: + enabled: false + endpoint: http://rtb.connektai.live/?pid={{SourceId}}&host={{Host}}&s=pbs + meta-info: + maintainer-email: adops@connekt.ai + adipolo: + enabled: false + endpoint: http://rtb.adipolo.live?pid={{SourceId}}&host={{Host}}&pbs=1 + meta-info: + maintainer-email: smart@adipolo.com + usersync: + enabled: true + cookie-family-name: adipolo + redirect: + url: https://sync.adipolo.live/psync?t=s&e=0&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&cb={{redirect_url}} + support-cors: false + uid-macro: '$UID' + iframe: + url: https://sync.adipolo.live/static/adisync.html?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&cb={{redirect_url}} + support-cors: false + uid-macro: '$UID' meta-info: maintainer-email: team@xe.works app-media-types: diff --git a/src/main/resources/bidder-config/yandex.yaml b/src/main/resources/bidder-config/yandex.yaml index f980fe3cb5e..11800ddb11a 100644 --- a/src/main/resources/bidder-config/yandex.yaml +++ b/src/main/resources/bidder-config/yandex.yaml @@ -7,11 +7,12 @@ adapters: app-media-types: site-media-types: - banner + - video - native vendor-id: 0 usersync: cookie-family-name: yandex redirect: - url: https://an.yandex.ru/mapuid/yandex/?ssp-id=10500&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&location={{redirect_url}} + url: https://yandex.ru/an/mapuid/yandex/?ssp-id=10500&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&location={{redirect_url}} support-cors: false uid-macro: '{YANDEXUID}' 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/zentotem.yaml b/src/main/resources/bidder-config/zentotem.yaml new file mode 100644 index 00000000000..0d6b0fe8121 --- /dev/null +++ b/src/main/resources/bidder-config/zentotem.yaml @@ -0,0 +1,17 @@ +adapters: + zentotem: + endpoint: https://rtb.zentotem.net/bid?sspuid=cqlnvfk00bhs0b6rci6g + endpoint-compression: gzip + modifying-vast-xml-allowed: true + meta-info: + maintainer-email: support@zentotem.net + 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/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..51da390a37d --- /dev/null +++ b/src/main/resources/bidder-config/zmaticoo.yaml @@ -0,0 +1,12 @@ +adapters: + zmaticoo: + endpoint: https://rtbbid.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/metrics-config/prometheus-labels.yaml b/src/main/resources/metrics-config/prometheus-labels.yaml index 2088021372c..b40139d6a8a 100644 --- a/src/main/resources/metrics-config/prometheus-labels.yaml +++ b/src/main/resources/metrics-config/prometheus-labels.yaml @@ -202,3 +202,9 @@ mappers: labels: adapter: ${0} action: ${1} + - match: adapter.*.activity.*.*.count + name: adapter.activity + labels: + adapter: ${0} + activity: ${1} + action: ${2} diff --git a/src/main/resources/static/bidder-params/adagio.json b/src/main/resources/static/bidder-params/adagio.json new file mode 100644 index 00000000000..af6a6bc4465 --- /dev/null +++ b/src/main/resources/static/bidder-params/adagio.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adagio Adapter Params", + "description": "A schema which validates params accepted by the Adagio adapter", + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "description": "Id of the Organization. Handed out by Adagio." + }, + "placement": { + "type": "string", + "description": "Refers to the placement of an adunit in a page. See Adagio recommended values: https://prebid.org/dev-docs/bidders/adagio.html#recommended-placement-param-values." + }, + "site": { + "type": "string", + "description": "Name of the site. Handed out by Adagio." + }, + "pagetype": { + "type": "string", + "description": "Describes what kind of content will be present in the page." + }, + "category": { + "type": "string", + "description": "Category of the content displayed in the page." + } + }, + "required": [ + "organizationId", + "placement" + ] +} 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/adoppler.json b/src/main/resources/static/bidder-params/adoppler.json deleted file mode 100644 index eaa4e6df80e..00000000000 --- a/src/main/resources/static/bidder-params/adoppler.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Adoppler Adapter Params", - "description": "A schema which validates params accepted by the Adoppler adapter", - "type": "object", - "properties": { - "adunit": { - "type": "string", - "description": "AdUnit to bid against to." - }, - "client": { - "type": "string", - "description": "Client name." - } - }, - "required": ["adunit"] -} 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/aduptech.json b/src/main/resources/static/bidder-params/aduptech.json new file mode 100644 index 00000000000..b2d7a8817ea --- /dev/null +++ b/src/main/resources/static/bidder-params/aduptech.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "AdUp Tech adapter params", + "description": "A schema which validates params accepted by the AdUp Tech adapter", + "type": "object", + "properties": { + "publisher": { + "type": "string", + "minLength": 1, + "description": "Unique publisher identifier." + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Unique placement identifier per publisher." + }, + "query": { + "type": "string", + "description": "Semicolon separated list of keywords." + }, + "adtest": { + "type": "boolean", + "description": "Deactivates tracking of impressions and clicks. **Should only be used for testing purposes!**" + }, + "debug": { + "type": "boolean", + "description": "Enables debug mode. **Should only be used for testing purposes!**" + }, + "ext": { + "type": "object", + "description": "Additional parameters to be included in the request." + } + }, + "required": [ + "publisher", + "placement" + ] +} diff --git a/src/main/resources/static/bidder-params/adverxo.json b/src/main/resources/static/bidder-params/adverxo.json new file mode 100644 index 00000000000..072abe4df99 --- /dev/null +++ b/src/main/resources/static/bidder-params/adverxo.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adverxo Adapter Params", + "description": "A schema which validates params accepted by the Adverxo adapter", + "type": "object", + "properties": { + "adUnitId": { + "type": "integer", + "minimum": 1, + "description": "Unique identifier for the ad unit in Adverxo platform." + }, + "auth": { + "type": "string", + "minLength": 6, + "description": "Authentication token provided by Adverxo platform for the AdUnit." + } + }, + "required": ["adUnitId", "auth"] +} diff --git a/src/main/resources/static/bidder-params/afront.json b/src/main/resources/static/bidder-params/afront.json new file mode 100644 index 00000000000..071d9772169 --- /dev/null +++ b/src/main/resources/static/bidder-params/afront.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Afront Adapter Params", + "description": "A schema which validates params accepted by the Afront adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "description": "Client account id", + "minLength": 1 + }, + "sourceId": { + "type": "string", + "description": "Data source id", + "minLength": 1 + } + }, + "required": [ + "accountId", + "sourceId" + ] +} diff --git a/src/main/resources/static/bidder-params/aidem.json b/src/main/resources/static/bidder-params/aidem.json index 01fddb3fc98..28afa68bfb4 100644 --- a/src/main/resources/static/bidder-params/aidem.json +++ b/src/main/resources/static/bidder-params/aidem.json @@ -17,7 +17,7 @@ "placementId": { "type": "string", "minLength": 1, - "description": "Unique publisher ttag ID" + "description": "Unique publisher tag ID" }, "rateLimit": { "type": "number", diff --git a/src/main/resources/static/bidder-params/akcelo.json b/src/main/resources/static/bidder-params/akcelo.json new file mode 100644 index 00000000000..2bdf75ce1d2 --- /dev/null +++ b/src/main/resources/static/bidder-params/akcelo.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Akcelo Adapter Params", + "description": "A schema which validates params accepted by the Akcelo adapter", + "type": "object", + "properties": { + "adUnitID": { + "type": "number", + "description": "The identifier of the ad unit. Will be provided by your account manager." + }, + "siteId": { + "type": "number", + "description": "The identifier of the site. Will be provided by your account manager." + }, + "test": { + "type": "number", + "description": "Whether to display test creatives or not. Default is 0." + } + }, + "required": [ + "adUnitId", + "siteId" + ] +} diff --git a/src/main/resources/static/bidder-params/appnexus.json b/src/main/resources/static/bidder-params/appnexus.json index 3c16fd7845d..8d3675765f3 100644 --- a/src/main/resources/static/bidder-params/appnexus.json +++ b/src/main/resources/static/bidder-params/appnexus.json @@ -27,7 +27,7 @@ "description": "Deprecated, use inv_code instead." }, "member": { - "type": "string", + "type": ["integer", "string"], "description": "An ID which identifies the member selling the impression." }, "keywords": { 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/bidtheatre.json b/src/main/resources/static/bidder-params/bidtheatre.json new file mode 100644 index 00000000000..f56899b7ccd --- /dev/null +++ b/src/main/resources/static/bidder-params/bidtheatre.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Bidtheatre Adapter Params", + "description": "A schema which validates params accepted by the Bidtheatre adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "description": "Publisher ID", + "format": "uuid" + } + }, + "required": [ + "publisherId" + ] +} 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/blis.json b/src/main/resources/static/bidder-params/blis.json new file mode 100644 index 00000000000..34d97b8a0e0 --- /dev/null +++ b/src/main/resources/static/bidder-params/blis.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Blis Adapter Params", + "description": "A schema which validates params accepted by the Blis adapter", + "type": "object", + "properties": { + "spid": { + "type": "string", + "minLength": 1, + "description": "Unique supply partner ID provided by Blis" + } + }, + "required": [ + "spid" + ] +} diff --git a/src/main/resources/static/bidder-params/blue.json b/src/main/resources/static/bidder-params/blue.json new file mode 100644 index 00000000000..db8680cb111 --- /dev/null +++ b/src/main/resources/static/bidder-params/blue.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Blue Adapter Params", + "description": "A schema which validates params accepted by the Blue adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID provided by Blue" + }, + "publisherId": { + "type": "string", + "description": "The publisher’s ID provided by Blue" + } + }, + "required": [ + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/boldwin_rapid.json b/src/main/resources/static/bidder-params/boldwin_rapid.json new file mode 100644 index 00000000000..b9b83340b19 --- /dev/null +++ b/src/main/resources/static/bidder-params/boldwin_rapid.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Boldwin-Rapid Adapter Params", + "description": "A schema which validates params accepted by the Boldwin-rapid adapter", + "type": "object", + "properties": { + "pid": { + "type": "string", + "minLength": 1, + "description": "Publisher ID" + }, + "tid": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + } + }, + "required": [ + "pid", + "tid" + ] +} 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/connatix.json b/src/main/resources/static/bidder-params/connatix.json new file mode 100644 index 00000000000..697e9cd1529 --- /dev/null +++ b/src/main/resources/static/bidder-params/connatix.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Connatix Adapter Params", + "description": "A schema which validates params accepted by the Connatix adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "viewabilityPercentage": { + "type": "number", + "description": "Declared viewability percentage (values from 0 to 1, where 1 = 100%)", + "minimum": 0, + "maximum": 1 + } + }, + "required": ["placementId"] +} + 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/contxtful.json b/src/main/resources/static/bidder-params/contxtful.json new file mode 100644 index 00000000000..18b44b46f58 --- /dev/null +++ b/src/main/resources/static/bidder-params/contxtful.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Contxtful Adapter Params", + "description": "A schema which validates params for the Contxtful adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID", + "minLength": 1 + }, + "customerId": { + "type": "string", + "description": "Customer ID used to build the endpoint URL", + "minLength": 1 + } + }, + "required": [ + "placementId", + "customerId" + ] +} 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/criteo.json b/src/main/resources/static/bidder-params/criteo.json index a44c2ae1cf1..5c78068ce9c 100644 --- a/src/main/resources/static/bidder-params/criteo.json +++ b/src/main/resources/static/bidder-params/criteo.json @@ -23,6 +23,16 @@ "type": "integer", "description": "Impression's network ID, preferred.", "minimum": 0 + }, + "pubid": { + "type": "string", + "description": "Impression's publisher ID.", + "minLength": 1 + }, + "uid": { + "type": "integer", + "description": "Impression's ad unit id.", + "minimum": 0 } }, "anyOf": [ diff --git a/src/main/resources/static/bidder-params/cwire.json b/src/main/resources/static/bidder-params/cwire.json new file mode 100644 index 00000000000..9b6e181b81f --- /dev/null +++ b/src/main/resources/static/bidder-params/cwire.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "CWire Adapter Params", + "description": "A schema which validates params accepted by the CWire adapter", + "type": "object", + "properties": { + "placementId": { + "type": "integer", + "description": "An ID which identifies this placement of the impression" + }, + "domainId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression" + }, + "pageId": { + "type": "integer", + "description": "An ID which identifies the site selling the impression (deprecated)" + }, + "cwcreative": { + "type": "string", + "description": "An CWire ID of the creative that we want to show" + }, + "cwdebug": { + "type": "boolean", + "description": "Enable CWire debug mode" + }, + "cwfeatures": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A string array of CWire features" + } + }, + "required": [] +} 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/elementaltv.json b/src/main/resources/static/bidder-params/elementaltv.json new file mode 100644 index 00000000000..f80e6c4f0d7 --- /dev/null +++ b/src/main/resources/static/bidder-params/elementaltv.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ElementalTV Adapter Params", + "description": "A schema which validates params accepted by the ElementalTV adapter", + "type": "object", + "properties": { + "adunit": { + "type": "string", + "description": "AdUnit to bid against to." + } + }, + "required": ["adunit"] +} 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/exco.json b/src/main/resources/static/bidder-params/exco.json new file mode 100644 index 00000000000..e60e43aa8d2 --- /dev/null +++ b/src/main/resources/static/bidder-params/exco.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "The Exco Adapter Params", + "description": "A schema which validates params accepted by Exco adapter", + "type": "object", + "properties": { + "accountId": { + "type": "string", + "minLength": 1, + "description": "A unique account identifier provided by EX.CO." + }, + "publisherId": { + "type": "string", + "minLength": 1, + "description": "Publisher ID provided by EX.CO." + }, + "tagId": { + "type": "string", + "minLength": 1, + "description": "A unique Tag ID (supply id) identifier provided by EX.CO." + } + }, + "required": [ + "accountId", + "publisherId", + "tagId" + ] +} diff --git a/src/main/resources/static/bidder-params/feedad.json b/src/main/resources/static/bidder-params/feedad.json new file mode 100644 index 00000000000..6049775e260 --- /dev/null +++ b/src/main/resources/static/bidder-params/feedad.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "FeedAd Adapter Params", + "description": "A schema which validates params accepted by the FeedAd adapter", + "properties": { + "clientToken": { + "description": "Your FeedAd client token. Check your FeedAd admin panel.", + "minLength": 1, + "type": "string" + }, + "decoration": { + "description": "A decoration to apply to the ad slot. See our documentation at https://docs.feedad.com/web/feed_ad/#decorations", + "type": "string" + }, + "placementId": { + "description": "A FeedAd placement ID of your choice", + "minLength": 1, + "pattern": "^(([a-z0-9])+[-_]?)+$", + "type": "string" + }, + "sdkOptions": { + "description": "Optional: Only required if you are using Prebid.JS in an app environment (aka hybrid app). See our documentation at https://docs.feedad.com/web/configuration/#hybrid-app-config-parameters", + "properties": { + "advertising_id": { + "type": "string", + "description": "Optional: The advertising id of the device or user (e.g. Apple IDFA, Google Advertising Client Id). We highly recommend setting this parameter to maximize your fill rate." + }, + "app_name": { + "type": "string", + "description": "The name of your app. This name will identify your app within the FeedAd admin dashboard." + }, + "bundle_id": { + "type": "string", + "description": "The unique package name or bundle id of your app." + }, + "hybrid_app": { + "type": "boolean", + "description": "Boolean indicating that the SDK is loaded within a hybrid app." + }, + "hybrid_platform": { + "description": "String identifying the device platform.", + "enum": [ + "", + "android", + "ios", + "windows" + ] + }, + "limit_ad_tracking": { + "type": "boolean", + "description": "Whether the app's user has limited ad tracking enabled." + } + }, + "type": [ + "object", + "null" + ] + } + }, + "required": [ + "clientToken", + "placementId" + ], + "type": "object" +} diff --git a/src/main/resources/static/bidder-params/flatads.json b/src/main/resources/static/bidder-params/flatads.json new file mode 100644 index 00000000000..9e36aa2c65c --- /dev/null +++ b/src/main/resources/static/bidder-params/flatads.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Flatads Adapter Params", + "description": "A schema which validates params accepted by the Flatads adapter", + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Token of the publisher", + "minLength": 1 + }, + "publisherId": { + "type": "string", + "description": "Flatads Publisher Id", + "minLength": 1 + } + }, + "required": [ + "token", + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/fwssp.json b/src/main/resources/static/bidder-params/fwssp.json new file mode 100644 index 00000000000..8c791621e76 --- /dev/null +++ b/src/main/resources/static/bidder-params/fwssp.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "FWSSP Adapter Params", + "description": "A schema which validates params accepted by the FWSSP adapter", + "type": "object", + "properties": { + "custom_site_section_id": { + "type": "string", + "description": "custom Site Section tag (e.g. ss_12345) or numeric Site Section ID (e.g. 12345)" + }, + "network_id": { + "type": "string", + "description": "Network ID (e.g. 12345)" + }, + "profile_id": { + "type": "string", + "description": "The value should contain a profile name. and NOT a numeric profile ID. This can either include the network ID prefix (e.g. 123456:profile_name_xyz123) or with the profile name alone (e.g. profile_name_xyz123)" + } + }, + "required": [ + "custom_site_section_id", + "network_id", + "profile_id" + ] +} 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/insticator.json b/src/main/resources/static/bidder-params/insticator.json new file mode 100644 index 00000000000..645ca8e0ebe --- /dev/null +++ b/src/main/resources/static/bidder-params/insticator.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Insticator Adapter Params", + "description": "A schema which validates params accepted by Insticator", + "type": "object", + "properties": { + "adUnitId": { + "type": "string", + "description": "Ad Unit Id", + "minLength": 1 + }, + "publisherId": { + "type": "string", + "description": "Publisher Id", + "minLength": 1 + } + }, + "required": [ + "adUnitId", + "publisherId" + ] +} diff --git a/src/main/resources/static/bidder-params/kobler.json b/src/main/resources/static/bidder-params/kobler.json new file mode 100644 index 00000000000..7e85601bfe8 --- /dev/null +++ b/src/main/resources/static/bidder-params/kobler.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Kobler Adapter Params", + "description": "A schema which validates params accepted by the Kobler adapter", + "type": "object", + + "properties": { + "test": { + "type": "boolean", + "description": "Whether the request is for testing only. When multiple ad units are submitted together, it is enough to set this parameter on the first one." + } + } +} diff --git a/src/main/resources/static/bidder-params/kueezrtb.json b/src/main/resources/static/bidder-params/kueezrtb.json new file mode 100644 index 00000000000..e11a79e028d --- /dev/null +++ b/src/main/resources/static/bidder-params/kueezrtb.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Kueez RTB Adapter Params", + "description": "A schema which validates params accepted by the Kueez RTB adapter", + "type": "object", + "properties": { + "cId": { + "type": "string", + "description": "The connection id.", + "minLength": 1, + "pattern": "^[a-zA-Z0-9_]+$" + } + }, + "required": [ + "cId" + ] +} 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/madsense.json b/src/main/resources/static/bidder-params/madsense.json new file mode 100644 index 00000000000..f45ac81f3ed --- /dev/null +++ b/src/main/resources/static/bidder-params/madsense.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "madSense Adapter Params", + "description": "A schema which validates params accepted by the madSense adapter", + "type": "object", + "properties": { + "company_id": { + "type": "string", + "description": "An id used to identify madSense company", + "minLength": 1 + } + }, + "required": [ + "company_id" + ] +} 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/mediasquare.json b/src/main/resources/static/bidder-params/mediasquare.json new file mode 100644 index 00000000000..ee8d3c67d0d --- /dev/null +++ b/src/main/resources/static/bidder-params/mediasquare.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mediasquare Adapter Params", + "description": "A schema which validates params accepted by the Mediasquare adapter", + "type": "object", + "properties": { + "owner": { + "type": "string", + "minLength": 1, + "description": "The owner provided for mediasquare." + }, + "code": { + "type": "string", + "minLength": 1, + "description": "The code provided for mediasquare." + } + }, + "required": [ + "owner", + "code" + ] +} 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..be5217efdb1 --- /dev/null +++ b/src/main/resources/static/bidder-params/missena.json @@ -0,0 +1,35 @@ +{ + "$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" + }, + "formats": { + "type": "array", + "description": "An array of formats to request (banner, native, or video)", + "items": { + "type": "string" + } + }, + "settings": { + "type": "object", + "description": "An object containing extra settings for the Missena adapter" + } + }, + "required": [ + "apiKey" + ] +} diff --git a/src/main/resources/static/bidder-params/mobilefuse.json b/src/main/resources/static/bidder-params/mobilefuse.json index aaab1aca295..5fe5d77c311 100644 --- a/src/main/resources/static/bidder-params/mobilefuse.json +++ b/src/main/resources/static/bidder-params/mobilefuse.json @@ -7,18 +7,9 @@ "placement_id": { "type": "integer", "description": "An ID which identifies this specific inventory placement" - }, - "pub_id": { - "type": "integer", - "description": "An ID which identifies the publisher selling the inventory." - }, - "tagid_src": { - "type": "string", - "description": "ext if passing publisher's ids, empty if passing MobileFuse IDs in placement_id field. Defaults to empty" } }, "required": [ - "placement_id", - "pub_id" + "placement_id" ] } diff --git a/src/main/resources/static/bidder-params/mobkoi.json b/src/main/resources/static/bidder-params/mobkoi.json new file mode 100644 index 00000000000..6858a48a679 --- /dev/null +++ b/src/main/resources/static/bidder-params/mobkoi.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Mobkoi Adapter Params", + "description": "A schema which validates params accepted by the Mobkoi adapter", + "type": "object", + "properties": { + "placementId": { + "type": "string", + "description": "Placement ID" + } + } +} diff --git a/src/main/resources/static/bidder-params/nativery.json b/src/main/resources/static/bidder-params/nativery.json new file mode 100644 index 00000000000..9f8dc02c274 --- /dev/null +++ b/src/main/resources/static/bidder-params/nativery.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Nativery Adapter Params", + "description": "A schema which validates params accepted by the Nativery adapter", + + "type": "object", + "properties": { + "widgetId": { + "type": "string", + "description": "An ID which identifies this Nativery widget" + } + }, + + "required": ["widgetId"] +} diff --git a/src/main/resources/static/bidder-params/nextmillennium.json b/src/main/resources/static/bidder-params/nextmillennium.json index 743d72f89ef..7b03c5087d2 100644 --- a/src/main/resources/static/bidder-params/nextmillennium.json +++ b/src/main/resources/static/bidder-params/nextmillennium.json @@ -13,6 +13,21 @@ "type": "string", "minLength": 1, "description": "An id used to identify NextMillennium placement group" + }, + "adSlots": { + "type": "array", + "minItems": 1, + "description": "IDs which identifies the ad slots", + "items": { + "type": "string" + } + }, + "allowedAds": { + "type": "array", + "description": "List of allowed ads", + "items": { + "type": "string" + } } }, "anyOf": [ diff --git a/src/main/resources/static/bidder-params/nexx360.json b/src/main/resources/static/bidder-params/nexx360.json new file mode 100644 index 00000000000..32e06db5297 --- /dev/null +++ b/src/main/resources/static/bidder-params/nexx360.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Nexx360 Adapter Params", + "description": "A schema which validates params accepted by the Nexx360 adapter", + "type": "object", + "properties": { + "tagId": { + "type": "string", + "minLength": 1, + "description": "TagId" + }, + "placement": { + "type": "string", + "minLength": 1, + "description": "Placement" + } + }, + "anyOf": [ + { + "required": [ + "tagId" + ] + }, + { + "required": [ + "placement" + ] + } + ] +} diff --git a/src/main/resources/static/bidder-params/ogury.json b/src/main/resources/static/bidder-params/ogury.json new file mode 100644 index 00000000000..b05902ee0a4 --- /dev/null +++ b/src/main/resources/static/bidder-params/ogury.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Ogury Adapter Params", + "description": "A schema which validates params accepted by the Ogury adapter", + "type": "object", + "properties": { + "assetKey": { + "type": [ + "string" + ], + "description": "The asset key provided by Ogury" + }, + "adUnitId": { + "type": [ + "string" + ], + "description": "Ad unit id configured with Ogury" + } + } +} diff --git a/src/main/resources/static/bidder-params/oms.json b/src/main/resources/static/bidder-params/oms.json index 1ab7e25eb7f..fa9dec36f61 100644 --- a/src/main/resources/static/bidder-params/oms.json +++ b/src/main/resources/static/bidder-params/oms.json @@ -6,11 +6,25 @@ "properties": { "pid": { "type": "string", - "description": "An id used to identify OMS publisher.", + "description": "Deprecated: An id used to identify OMS publisher.", "minLength": 5 + }, + "publisherId": { + "type": "integer", + "description": "An ID used to identify OMS publisher.", + "minimum": 10000 } }, - "required": [ - "pid" + "oneOf": [ + { + "required": [ + "pid" + ] + }, + { + "required": [ + "publisherId" + ] + } ] } 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/optidigital.json b/src/main/resources/static/bidder-params/optidigital.json new file mode 100644 index 00000000000..4cde58a8100 --- /dev/null +++ b/src/main/resources/static/bidder-params/optidigital.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Optidigital Adapter Params", + "description": "A schema which validates params accepted by the Optidigital adapter", + "type": "object", + "properties": { + "publisherId": { + "type": "string", + "minLength": 2, + "description": "Publisher ID" + }, + "placementId": { + "type": "string", + "minLength": 1, + "description": "Placement ID" + }, + "divId": { + "type": "string", + "description": "Div ID" + }, + "pageTemplate": { + "type": "string", + "description": "Page Template" + } + }, + "required": [ + "publisherId", + "placementId" + ] +} 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/rediads.json b/src/main/resources/static/bidder-params/rediads.json new file mode 100644 index 00000000000..13a8f9b4cbe --- /dev/null +++ b/src/main/resources/static/bidder-params/rediads.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "RediAds Adapter Params", + "description": "A schema which validates params accepted by the Rediads adapter", + "type": "object", + "properties": { + "account_id": { + "type": "string" + }, + "slot": { + "type": "string" + }, + "endpoint": { + "type": "string" + } + }, + "required": [ + "account_id" + ] +} 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/seedtag.json b/src/main/resources/static/bidder-params/seedtag.json new file mode 100644 index 00000000000..8d84b059fd0 --- /dev/null +++ b/src/main/resources/static/bidder-params/seedtag.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Seedtag Adapter Params", + "description": "A schema which validates params accepted by the Seedtag adapter", + "type": "object", + "properties": { + "adUnitId": { + "type": "string", + "description": "Ad Unit ID", + "minLength": 1 + } + }, + "required": [ + "adUnitId" + ] +} diff --git a/src/main/resources/static/bidder-params/showheroes.json b/src/main/resources/static/bidder-params/showheroes.json new file mode 100644 index 00000000000..3c269d118d8 --- /dev/null +++ b/src/main/resources/static/bidder-params/showheroes.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Showheroes Adapter Params", + "description": "A schema which validates params accepted by the Showheroes adapter", + "type": "object", + "properties": { + "unitId": { + "type": "string", + "description": "Unit ID", + "minLength": 8 + } + }, + "required": ["unitId"] +} diff --git a/src/main/resources/static/bidder-params/smarthub.json b/src/main/resources/static/bidder-params/smarthub.json index 24a8da771dd..1a68bf5cfc3 100644 --- a/src/main/resources/static/bidder-params/smarthub.json +++ b/src/main/resources/static/bidder-params/smarthub.json @@ -1,13 +1,12 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "SmartHub Adapter Params", - "description": "A schema which validates params accepted by the SmartHub adapter", + "title": "Attekmi (formerly SmartHub) Adapter Params", + "description": "A schema which validates params accepted by the Attekmi (formerly SmartHub) adapter", "type": "object", "properties": { "partnerName": { "type": "string", - "description": "SmartHub unique partner name", - "minLength": 1 + "description": "Attekmi (formerly SmartHub) unique partner name" }, "seat": { "type": "string", @@ -21,7 +20,6 @@ } }, "required": [ - "partnerName", "seat", "token" ] diff --git a/src/main/resources/static/bidder-params/smoot.json b/src/main/resources/static/bidder-params/smoot.json new file mode 100644 index 00000000000..017047107cf --- /dev/null +++ b/src/main/resources/static/bidder-params/smoot.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smoot Adapter Params", + "description": "A schema which validates params accepted by the Smoot 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/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/sparteo.json b/src/main/resources/static/bidder-params/sparteo.json new file mode 100644 index 00000000000..ca8c072ae77 --- /dev/null +++ b/src/main/resources/static/bidder-params/sparteo.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Sparteo Params", + "type": "object", + "properties": { + "networkId": { + "type": "string", + "description": "Sparteo network ID. This information will be given to you by the Sparteo team." + }, + "custom1": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom2": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom3": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom4": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + }, + "custom5": { + "type": "string", + "description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore." + } + }, + "required": [ + "networkId" + ] +} \ No newline at end of file diff --git a/src/main/resources/static/bidder-params/startio.json b/src/main/resources/static/bidder-params/startio.json new file mode 100644 index 00000000000..bae19ac4e81 --- /dev/null +++ b/src/main/resources/static/bidder-params/startio.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Start.io Adapter Params", + "description": "A schema which validates params accepted by the Start.io adapter", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/src/main/resources/static/bidder-params/stroeerCore.json b/src/main/resources/static/bidder-params/stroeerCore.json index e693962fa97..a5a93df64b8 100644 --- a/src/main/resources/static/bidder-params/stroeerCore.json +++ b/src/main/resources/static/bidder-params/stroeerCore.json @@ -9,5 +9,7 @@ "description": "Slot Id" } }, - "required": ["sid"] + "required": [ + "sid" + ] } diff --git a/src/main/resources/static/bidder-params/taboola.json b/src/main/resources/static/bidder-params/taboola.json index 25596db2b75..25c126603ab 100644 --- a/src/main/resources/static/bidder-params/taboola.json +++ b/src/main/resources/static/bidder-params/taboola.json @@ -2,6 +2,7 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Taboola Adapter Params", "description": "A schema which validates params accepted by the Taboola adapter", + "type": "object", "properties": { "publisherId": { @@ -11,7 +12,12 @@ "type": "string" }, "tagid": { - "type": "string" + "type": "string", + "description": "Deprecated, use tagId instead." + }, + "tagId": { + "type": "string", + "description": "preferred, will get precedence if both tagId and tagid are defined" }, "bidfloor": { "type": "number" @@ -35,8 +41,12 @@ "type": "integer" } }, - "required": [ - "tagid", - "publisherId" + "oneOf": [ + { + "required": [ "tagid", "publisherId" ] + }, + { + "required": [ "tagId", "publisherId" ] + } ] } diff --git a/src/main/resources/static/bidder-params/teqblaze.json b/src/main/resources/static/bidder-params/teqblaze.json new file mode 100644 index 00000000000..66fbaba7526 --- /dev/null +++ b/src/main/resources/static/bidder-params/teqblaze.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Teqblaze Adapter Params", + "description": "A schema which validates params accepted by the TeqBlaze 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/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..566e7e556e6 --- /dev/null +++ b/src/main/resources/static/bidder-params/thetradedesk.json @@ -0,0 +1,19 @@ +{ + "$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", + "minLength": 1, + "description": "An ID which identifies the publisher" + }, + "supplySourceId": { + "type":"string", + "minLength": 1, + "description": "An ID provided by TheTradeDesk used to determine which endpoint to use" + } + }, + "required": ["publisherId"] +} diff --git a/src/main/resources/static/bidder-params/thirtythreeacross.json b/src/main/resources/static/bidder-params/thirtythreeacross.json index 7524aca3a7f..daa61cf31c9 100644 --- a/src/main/resources/static/bidder-params/thirtythreeacross.json +++ b/src/main/resources/static/bidder-params/thirtythreeacross.json @@ -10,15 +10,25 @@ }, "siteId": { "type": "string", - "description": "Site Id" + "description": "[Deprecated use zoneId instead] Site Id" }, "zoneId": { "type": "string", "description": "Zone Id" } }, - "required": [ - "productId", - "siteId" + "anyOf": [ + { + "required": [ + "productId", + "zoneId" + ] + }, + { + "required": [ + "productId", + "siteId" + ] + } ] } 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..345662fe456 --- /dev/null +++ b/src/main/resources/static/bidder-params/tradplus.json @@ -0,0 +1,20 @@ +{ + "$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" + ] +} 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/zentotem.json b/src/main/resources/static/bidder-params/zentotem.json new file mode 100644 index 00000000000..de59fc47b70 --- /dev/null +++ b/src/main/resources/static/bidder-params/zentotem.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Zentotem Adapter Params", + "description": "A schema which validates params accepted by the Zentotem adapter", + "type": "object", + "properties": {} +} 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..19a29ae0058 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,12 @@ 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"), + PB_REQUEST_CORRECTION('pb-request-correction'), + OPTABLE_TARGETING('optable-targeting'), + PB_RULE_ENGINE('pb-rule-engine') @JsonValue final String code diff --git a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy index 8bbda5dd297..d721255741d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/UidsCookie.groovy @@ -16,11 +16,11 @@ class UidsCookie { Map tempUIDs Boolean optout - static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC) { + static UidsCookie getDefaultUidsCookie(BidderName bidder = GENERIC, Integer daysUntilExpiry = 2) { new UidsCookie().tap { uids = [(bidder): UUID.randomUUID().toString()] tempUIDs = [(bidder): new UidWithExpiry(uid: UUID.randomUUID().toString(), - expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(2))] + expires: ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry))] } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/AppNexus.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/AppNexus.groovy index 3756f912105..7b6f2077473 100644 --- a/src/test/groovy/org/prebid/server/functional/model/bidder/AppNexus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/AppNexus.groovy @@ -14,6 +14,7 @@ class AppNexus implements BidderAdapter { String trafficSourceCode Boolean isAmp String hbSource + Double reserve static AppNexus getDefault() { new AppNexus().tap { 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..702918fa886 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,21 +5,31 @@ import net.minidev.json.annotate.JsonIgnore enum BidderName { + WILDCARD("*"), UNKNOWN("unknown"), + EMPTY(""), BOGUS("bogus"), ALIAS("alias"), + ALIAS_CAMEL_CASE("AlIaS"), + ALIAS_UPPER_CASE("ALIAS"), GENERIC_CAMEL_CASE("GeNerIc"), GENERIC("generic"), + GENER_X("gener_x"), RUBICON("rubicon"), APPNEXUS("appnexus"), RUBICON_ALIAS("rubiconAlias"), OPENX("openx"), + OPENX_ALIAS("openxalias"), ACEEX("aceex"), ACUITYADS("acuityads"), AAX("aax"), ADKERNEL("adkernel"), + IX("ix"), GRID("grid"), - MEDIANET("medianet") + MEDIANET("medianet"), + AMX("amx"), + AMX_CAMEL_CASE("AmX"), + AMX_UPPER_CASE("AMX"), @JsonValue final String value 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..b583363974c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/bidder/GeneralBidderAdapter.groovy @@ -0,0 +1,14 @@ +package org.prebid.server.functional.model.bidder + +import com.fasterxml.jackson.annotation.JsonProperty + +class GeneralBidderAdapter extends Generic { + + String siteId + List size + String sid + @JsonProperty("ds") + String demandSource + @JsonProperty("bc") + BidderName bidderCode +} 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/AbTest.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy new file mode 100644 index 00000000000..baa19a80db4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy @@ -0,0 +1,31 @@ +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 AbTest { + + Boolean enabled + String moduleCode + @JsonProperty("module_code") + String moduleCodeSnakeCase + Set accounts + Integer percentActive + @JsonProperty("percent_active") + Integer percentActiveSnakeCase + Boolean logAnalyticsTag + @JsonProperty("log_analytics_tag") + Boolean logAnalyticsTagSnakeCase + + static AbTest getDefault(String moduleCode, List accounts = null) { + new AbTest(enabled: true, + moduleCode: moduleCode, + accounts: accounts, + percentActive: 0, + logAnalyticsTag: true) + } +} 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..2dc5ff7c77b 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 @@ -5,6 +5,9 @@ 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.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidRounding +import org.prebid.server.functional.model.request.auction.PaaFormat import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.MediaType @@ -12,18 +15,48 @@ 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 + AccountCacheConfig cache + AccountRankingConfig ranking AccountPriceFloorsConfig priceFloors + AccountProfilesConfigs profiles Targeting targeting + PaaFormat paaformat @JsonProperty("preferredmediatype") Map preferredMediaType @JsonProperty("privacysandbox") PrivacySandbox privacySandbox + @JsonProperty("bidadjustments") + BidAdjustment bidAdjustments + BidRounding bidRounding + Integer impressionLimit + + @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 + @JsonProperty("bid_rounding") + BidRounding bidRoundingSnakeCase + @JsonProperty("impression_limit") + Integer impressionLimitSnakeCase + } 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/AccountCacheConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy new file mode 100644 index 00000000000..121e32f03cd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountCacheConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountCacheConfig { + + Boolean enabled +} 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..83912535a57 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,13 @@ class AccountConfig { AccountMetricsConfig metrics AccountCookieSyncConfig cookieSync AccountHooksConfiguration hooks + AccountSetting settings + @JsonProperty("cookie_sync") + AccountCookieSyncConfig cookieSyncSnakeCase + AlternateBidderCodes alternateBidderCodes + @JsonProperty("alternate_bidder_codes") + AlternateBidderCodes alternateBidderCodesSnakeCase + AccountVtrackConfig vtrack 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/AccountGppConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy index add9834d5c5..bb4e0ab0b74 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountGppConfig.groovy @@ -8,5 +8,6 @@ class AccountGppConfig { PrivacyModule code Boolean enabled + Integer skipRate GppModuleConfig config } 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..bab4ec983a3 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,8 @@ import groovy.transform.ToString class AccountHooksConfiguration { ExecutionPlan executionPlan + @JsonProperty("execution_plan") + ExecutionPlan executionPlanSnakeCase PbsModulesConfig modules + AdminConfig admin } 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..c83c69280fa 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,19 @@ class AccountPriceFloorsConfig { Boolean adjustForBidAdjustment Boolean enforceDealFloors Boolean useDynamicData + Long maxRules + Long maxSchemaDims + + @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 + @JsonProperty("max_rules") + Long maxRulesSnakeCase + @JsonProperty("max_schema_dims") + Long maxSchemaDimsSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy new file mode 100644 index 00000000000..71852279fb8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountProfilesConfigs.groovy @@ -0,0 +1,17 @@ +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 AccountProfilesConfigs { + + Integer limit + Boolean failOnUnknown + + @JsonProperty("fail_on_unknown") + Boolean failOnUnknownSnakeCase +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy new file mode 100644 index 00000000000..64103717127 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountRankingConfig { + + Boolean enabled +} 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/AccountVtrackConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy new file mode 100644 index 00000000000..77397e20b4c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountVtrackConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountVtrackConfig { + + Integer ttl +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ActivityConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ActivityConfig.groovy index 14f19d03331..fbd7ba4b81b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ActivityConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ActivityConfig.groovy @@ -3,9 +3,9 @@ package org.prebid.server.functional.model.config import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.ActivityType -import static org.prebid.server.functional.model.config.DataActivity.INVALID import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR import static org.prebid.server.functional.model.config.UsNationalPrivacySection.GPC +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.INVALID @ToString(includeNames = true, ignoreNulls = true) class ActivityConfig { diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy new file mode 100644 index 00000000000..755a47bcbaa --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.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 +import org.prebid.server.functional.model.ModuleName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AdminConfig { + + Map moduleExecution +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AlternateBidderCodes.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AlternateBidderCodes.groovy new file mode 100644 index 00000000000..465acaad2cc --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AlternateBidderCodes.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.EqualsAndHashCode +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AlternateBidderCodes { + + Boolean enabled + Map bidders +} 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/Audience.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy new file mode 100644 index 00000000000..9ea6345ff0e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/Audience.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class Audience { + + String provider + List ids +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy new file mode 100644 index 00000000000..e964b9f84b5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AudienceId.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AudienceId { + + String id +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/BidderConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/BidderConfig.groovy new file mode 100644 index 00000000000..e139419e4e3 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/BidderConfig.groovy @@ -0,0 +1,21 @@ +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 +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@EqualsAndHashCode +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class BidderConfig { + + Boolean enabled + List allowedBidderCodes + @JsonProperty("allowedbiddercodes") + List allowedBidderCodesLowerCase + @JsonProperty("allowed_bidder_codes") + List allowedBidderCodesSnakeCase +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy b/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy new file mode 100644 index 00000000000..e16548b57c5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/CacheProperties.groovy @@ -0,0 +1,21 @@ +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.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class CacheProperties { + + Boolean enabled + Integer ttlSeconds + + static CacheProperties getDefault() { + new CacheProperties().tap { + enabled = true + ttlSeconds = PBSUtils.getRandomNumber(0, 1000) + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ConfigCase.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ConfigCase.groovy new file mode 100644 index 00000000000..f3dc523371c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/ConfigCase.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.config + +enum ConfigCase { + + CAMEL_CASE, KEBAB_CASE, SNAKE_CASE +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/DataActivity.groovy b/src/test/groovy/org/prebid/server/functional/model/config/DataActivity.groovy deleted file mode 100644 index 64434cb67f7..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/config/DataActivity.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package org.prebid.server.functional.model.config - -import com.fasterxml.jackson.annotation.JsonValue - -enum DataActivity { - - NOT_APPLICABLE(0), - NOTICE_PROVIDED(1), - NOTICE_NOT_PROVIDED(2), - NO_CONSENT(1), - CONSENT(2), - INVALID(-1), - - @JsonValue - final int dataActivityBits - - DataActivity(int dataActivityBits) { - this.dataActivityBits = dataActivityBits - } - - static DataActivity fromInt(int dataActivityBits) { - values().find { it.dataActivityBits == dataActivityBits } - ?: { throw new IllegalArgumentException("Invalid dataActivityBits: ${dataActivityBits}") } - } -} 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..9ded40849d0 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,20 @@ 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) + } + + static EndpointExecutionPlan getModulesEndpointExecutionPlan(Map> modulesStages) { + new EndpointExecutionPlan( + stages: modulesStages.collectEntries { stage, moduleNames -> + [(stage): new StageExecutionPlan( + groups: moduleNames.collect { moduleName -> + ExecutionGroup.getModuleExecutionGroup(moduleName, stage) + } + )] + } as Map + ) } } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/EqualityValueRule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/EqualityValueRule.groovy index 5e7cbf33476..0dd3acebe7d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/EqualityValueRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/EqualityValueRule.groovy @@ -7,12 +7,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import groovy.transform.ToString +import org.prebid.server.functional.model.privacy.gpp.GppDataActivity @ToString(includeNames = true, ignoreNulls = true) @JsonDeserialize(using = EqualityValueRuleDeserialize.class) class EqualityValueRule extends ValueRestrictedRule { - EqualityValueRule(UsNationalPrivacySection privacySection, DataActivity value) { + EqualityValueRule(UsNationalPrivacySection privacySection, GppDataActivity value) { super(privacySection, value) } @@ -22,7 +23,7 @@ class EqualityValueRule extends ValueRestrictedRule { EqualityValueRule deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { JsonNode node = jsonParser.getCodec().readTree(jsonParser) def privacySection = UsNationalPrivacySection.valueFromText(node?.get(0)?.get(JSON_LOGIC_VALUE_FIELD)?.textValue()) - def value = DataActivity.fromInt(node?.get(1)?.asInt()) + def value = GppDataActivity.fromInt(node?.get(1)?.asInt()) return new EqualityValueRule(privacySection, value) } } 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..eb32b75e729 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 = 1000 + 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..653f8c8cbea 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 @@ -1,14 +1,22 @@ 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.ModuleName @ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class ExecutionPlan { + List abTests 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)]) } + + static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, Map> modulesStages) { + new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModulesEndpointExecutionPlan(modulesStages)]) + } } 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..ea6899fbb25 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,23 +1,57 @@ 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 +import static org.prebid.server.functional.model.config.ConfigCase.CAMEL_CASE +import static org.prebid.server.functional.model.config.ConfigCase.KEBAB_CASE +import static org.prebid.server.functional.model.config.ConfigCase.SNAKE_CASE + @ToString(includeNames = true, ignoreNulls = true) 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 + Boolean allowPersonalDataConsent2 + @JsonProperty("allow_personal_data_consent_2") + Boolean allowPersonalDataConsent2SnakeCase + @JsonProperty("allow-personal-data-consent-2") + Boolean allowPersonalDataConsent2KebabCase static GppModuleConfig getDefaultModuleConfig(ActivityConfig activityConfig = ActivityConfig.configWithDefaultRestrictRules, - List sids = [GppSectionId.USP_NAT_V1], - Boolean normalizeFlags = true) { - new GppModuleConfig().tap { - it.activityConfig = [activityConfig] - it.sids = sids - it.normalizeFlags = normalizeFlags + List sids = [GppSectionId.US_NAT_V1], + Boolean normalizeFlags = true, + ConfigCase configCase = CAMEL_CASE) { + new GppModuleConfig(sids: sids).tap { + switch (configCase) { + case CAMEL_CASE -> { + it.activityConfig = [activityConfig] + it.normalizeFlags = normalizeFlags + } + case KEBAB_CASE -> { + it.activityConfigKebabCase = [activityConfig] + it.normalizeFlagsKebabCase = normalizeFlags + } + case SNAKE_CASE -> { + it.activityConfigSnakeCase = [activityConfig] + it.normalizeFlagsSnakeCase = 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/IdentifierType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy new file mode 100644 index 00000000000..7148c869c77 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/IdentifierType.groovy @@ -0,0 +1,49 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +import static org.prebid.server.functional.model.config.OperatingSystem.ANDROID +import static org.prebid.server.functional.model.config.OperatingSystem.FIRE +import static org.prebid.server.functional.model.config.OperatingSystem.IOS +import static org.prebid.server.functional.model.config.OperatingSystem.ROKU +import static org.prebid.server.functional.model.config.OperatingSystem.TIZEN + +enum IdentifierType { + + EMAIL_ADDRESS("e"), + PHONE_NUMBER("p"), + POSTAL_CODE("z"), + APPLE_IDFA("a"), + GOOGLE_GAID("g"), + ROKU_RIDA("r"), + SAMSUNG_TIFA("s"), + AMAZON_AFAI("f"), + NET_ID("n"), + ID5("id5"), + UTIQ("utiq"), + OPTABLE_VID("v") + + @JsonValue + final String value + + IdentifierType(String value) { + this.value = value + } + + static IdentifierType fromOS(OperatingSystem os) { + switch (os) { + case IOS: + return APPLE_IDFA + case ANDROID: + return GOOGLE_GAID + case ROKU: + return ROKU_RIDA + case TIZEN: + return SAMSUNG_TIFA + case FIRE: + return AMAZON_AFAI + default: + throw new IllegalArgumentException("Unsupported OS: " + os); + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/InequalityValueRule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/InequalityValueRule.groovy index b9fcfcb6d67..e65ecc3421e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/InequalityValueRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/InequalityValueRule.groovy @@ -7,12 +7,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize import groovy.transform.ToString +import org.prebid.server.functional.model.privacy.gpp.GppDataActivity @ToString(includeNames = true, ignoreNulls = true) @JsonDeserialize(using = InequalityValueRuleDeserialize.class) class InequalityValueRule extends ValueRestrictedRule { - InequalityValueRule(UsNationalPrivacySection privacySection, DataActivity value) { + InequalityValueRule(UsNationalPrivacySection privacySection, GppDataActivity value) { super(privacySection, value) } @@ -22,7 +23,7 @@ class InequalityValueRule extends ValueRestrictedRule { InequalityValueRule deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { JsonNode node = jsonParser.getCodec().readTree(jsonParser) def privacySection = UsNationalPrivacySection.valueFromText(node?.get(0)?.get(JSON_LOGIC_VALUE_FIELD)?.textValue()) - def value = DataActivity.fromInt(node?.get(1)?.asInt()) + def value = GppDataActivity.fromInt(node?.get(1)?.asInt()) return new InequalityValueRule(privacySection, value) } } 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..af7bf670c95 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy @@ -0,0 +1,27 @@ +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"), + PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"), + OPTABLE_TARGETING_PROCESSED_AUCTION_REQUEST("optable-targeting-processed-auction-request-hook"), + PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST("pb-rule-engine-processed-auction-request") + + @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/OperatingSystem.groovy b/src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy new file mode 100644 index 00000000000..f10e28a4cb6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/OperatingSystem.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum OperatingSystem { + + IOS("ios"), + ANDROID("android"), + ROKU("roku"), + TIZEN("tizen"), + FIRE("fire") + + @JsonValue + final String value + + OperatingSystem(String value) { + this.value = value; + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy new file mode 100644 index 00000000000..56e6f3415b8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/OptableTargetingConfig.groovy @@ -0,0 +1,33 @@ +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.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class OptableTargetingConfig { + + String apiEndpoint + String apiKey + String tenant + String origin + Map ppidMapping + Boolean adserverTargeting + Long timeout + String idPrefixOrder + CacheProperties cache + + static OptableTargetingConfig getDefault(Map ppidMapping) { + new OptableTargetingConfig().tap { + it.apiKey = PBSUtils.randomString + it.tenant = PBSUtils.randomString + it.origin = PBSUtils.randomString + it.apiEndpoint = PBSUtils.randomString + it.adserverTargeting = true + it.ppidMapping = ppidMapping + it.cache = CacheProperties.default + } + } +} 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/PbRequestCorrectionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy new file mode 100644 index 00000000000..5d7a980115b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy @@ -0,0 +1,29 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class PbRequestCorrectionConfig { + + @JsonProperty("pbsdkAndroidInstlRemove") + Boolean interstitialCorrectionEnabled + @JsonProperty("pbsdkUaCleanup") + Boolean userAgentCorrectionEnabled + @JsonProperty("pbsdk-android-instl-remove") + Boolean interstitialCorrectionEnabledKebabCase + @JsonProperty("pbsdk-ua-cleanup") + Boolean userAgentCorrectionEnabledKebabCase + + Boolean enabled + + static PbRequestCorrectionConfig getDefaultConfigWithInterstitial(Boolean interstitialCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, interstitialCorrectionEnabled: interstitialCorrectionEnabled) + } + + static PbRequestCorrectionConfig getDefaultConfigWithUserAgentCorrection(Boolean userAgentCorrectionEnabled = true, + Boolean enabled = true) { + new PbRequestCorrectionConfig(enabled: enabled, userAgentCorrectionEnabled: userAgentCorrectionEnabled) + } +} 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/PbRulesEngine.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRulesEngine.groovy new file mode 100644 index 00000000000..321c548394b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRulesEngine.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.config + +import java.time.ZonedDateTime + +class PbRulesEngine { + + Boolean enabled + Boolean generateRulesFromBidderConfig + ZonedDateTime timestamp + List ruleSets + + static PbRulesEngine createRulesEngineWithRule(Boolean enabled = true) { + new PbRulesEngine().tap { + it.enabled = enabled + it.generateRulesFromBidderConfig = false + it.ruleSets = [RuleSet.createRuleSets()] + } + } +} 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..ac2685742b6 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,9 @@ import org.prebid.server.functional.model.request.auction.RichmediaFilter class PbsModulesConfig { RichmediaFilter pbRichmediaFilter + Ortb2BlockingConfig ortb2Blocking + PbResponseCorrection pbResponseCorrection + PbRequestCorrectionConfig pbRequestCorrection + OptableTargetingConfig optableTargeting + PbRulesEngine pbRuleEngine } 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..89f32a951a4 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,21 @@ 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 + Integer maxSchemaDims + @JsonProperty("max_schema_dims") + Integer maxSchemaDimsSnakeCase } 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/ResultFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ResultFunction.groovy new file mode 100644 index 00000000000..108e8a00fe0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/ResultFunction.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum ResultFunction { + + INCLUDE_BIDDERS("includeBidders"), + EXCLUDE_BIDDER("excludeBidders"), + LOG_A_TAG("logAtag") + + String value + + ResultFunction(String value) { + this.value = value + } + + @Override + @JsonValue + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy new file mode 100644 index 00000000000..20f5855be7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineArguments.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.config + +import org.prebid.server.functional.model.bidder.BidderName + +class RuleEngineArguments { + + List bidders + Integer seatNonBid + Boolean ifSyncedId + String analyticsValue +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy new file mode 100644 index 00000000000..b67b4dac362 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunction.groovy @@ -0,0 +1,44 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue + +enum RuleEngineFunction { + + DEVICE_COUNTRY("deviceCountry", null), + DEVICE_COUNTRY_IN("deviceCountryIn", "countries"), + DATA_CENTER("dataCenter", null), + DATA_CENTER_IN("dataCenterIn", "datacenters"), + CHANNEL("channel", null), + EID_AVAILABLE("eidAvailable", null), + EID_IN("eidIn", "sources"), + USER_FPD_AVAILABLE("userFpdAvailable", null), + FPD_AVAILABLE("fpdAvailable", null), + GPP_SID_AVAILABLE("gppSidAvailable", null), + GPP_SID_IN("gppSidIn", "sids"), + TCF_IN_SCOPE("tcfInScope", null), + PERCENT("percent", "pct"), + PREBID_KEY("prebidKey", "key"), + DOMAIN("domain", null), + DOMAIN_IN("domainIn", "domains"), + BUNDLE("bundle", null), + BUNDLE_IN("bundleIn", "bundles"), + MEDIA_TYPE_IN("mediaTypeIn", "types"), + AD_UNIT_CODE("adUnitCode", null), + AD_UNIT_CODE_IN("adUnitCodeIn", "codes"), + DEVICE_TYPE("deviceType", null), + DEVICE_TYPE_IN("deviceTypeIn", "types") + + private String value + private String fieldName + + RuleEngineFunction(String value, String fieldName) { + this.value = value + this.fieldName = fieldName + } + + @JsonValue + @Override + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy new file mode 100644 index 00000000000..a2cb809d0fe --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineFunctionArgs.groovy @@ -0,0 +1,37 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.util.PBSUtils + +class RuleEngineFunctionArgs { + + List countries + List datacenters + List sources + List sids + @JsonProperty("pct") + Object percent + Object key + List domains + List bundles + List codes + List types + String operator + BigDecimal value + Currency currency + + static RuleEngineFunctionArgs getDefaultFunctionArgs() { + new RuleEngineFunctionArgs().tap { + countries = [PBSUtils.randomString] + datacenters = [PBSUtils.randomString] + sources = [PBSUtils.randomString] + sids = [PBSUtils.randomNumber] + percent = PBSUtils.getRandomNumber(1, 100) + key = PBSUtils.randomString + domains = [PBSUtils.randomString] + bundles = [PBSUtils.randomString] + codes = [PBSUtils.randomString] + types = [PBSUtils.randomString] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy new file mode 100644 index 00000000000..7193bb920dd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefault.groovy @@ -0,0 +1,7 @@ +package org.prebid.server.functional.model.config + +class RuleEngineModelDefault { + + ResultFunction function + RuleEngineModelDefaultArgs args +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy new file mode 100644 index 00000000000..3d7884926de --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelDefaultArgs.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.config + +class RuleEngineModelDefaultArgs { + + String analyticsValue +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy new file mode 100644 index 00000000000..557fc999e61 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRule.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.config + +import static java.lang.Boolean.TRUE +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult + +class RuleEngineModelRule { + + List conditions + List results + + static RuleEngineModelRule createRuleEngineModelRule() { + new RuleEngineModelRule().tap { + it.conditions = [TRUE as String] + it.results = [createRuleEngineModelRuleWithExcludeResult()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy new file mode 100644 index 00000000000..cb898171623 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResult.groovy @@ -0,0 +1,38 @@ +package org.prebid.server.functional.model.config + +import org.prebid.server.functional.model.bidder.BidderName + +import static org.prebid.server.functional.model.bidder.BidderName.ACEEX +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.config.ResultFunction.EXCLUDE_BIDDER +import static org.prebid.server.functional.model.config.ResultFunction.INCLUDE_BIDDERS +import static org.prebid.server.functional.model.config.ResultFunction.LOG_A_TAG + +class RuleEngineModelRuleResult { + + ResultFunction function + RuleEngineModelRuleResultsArgs args + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithIncludeResult(BidderName bidderName = ACEEX, + Boolean ifSyncedId = false) { + new RuleEngineModelRuleResult().tap { + it.function = INCLUDE_BIDDERS + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgs(bidderName, ifSyncedId) + } + } + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithExcludeResult(BidderName bidderName = OPENX, + Boolean ifSyncedId = false) { + new RuleEngineModelRuleResult().tap { + it.function = EXCLUDE_BIDDER + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgs(bidderName, ifSyncedId) + } + } + + static RuleEngineModelRuleResult createRuleEngineModelRuleWithLogATagResult() { + new RuleEngineModelRuleResult().tap { + it.function = LOG_A_TAG + it.args = RuleEngineModelRuleResultsArgs.createRuleEngineModelRuleResultsArgsOnlyATag() + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy new file mode 100644 index 00000000000..c1ec70eb853 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelRuleResultsArgs.groovy @@ -0,0 +1,31 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.response.auction.BidRejectionReason +import org.prebid.server.functional.util.PBSUtils + +class RuleEngineModelRuleResultsArgs { + + List bidders + @JsonProperty("seatnonbid") + BidRejectionReason seatNonBid + @JsonProperty("analyticsValue") + String analyticsValue + @JsonProperty("ifSyncedId") + Boolean ifSyncedId + + static RuleEngineModelRuleResultsArgs createRuleEngineModelRuleResultsArgs(BidderName bidderName, Boolean ifSyncedId) { + new RuleEngineModelRuleResultsArgs().tap { + it.bidders = [bidderName] + it.analyticsValue = PBSUtils.randomString + it.ifSyncedId = ifSyncedId + } + } + + static RuleEngineModelRuleResultsArgs createRuleEngineModelRuleResultsArgsOnlyATag() { + new RuleEngineModelRuleResultsArgs().tap { + it.analyticsValue = PBSUtils.randomString + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy new file mode 100644 index 00000000000..299ad225a89 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleEngineModelSchema.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY_IN +import static org.prebid.server.functional.model.pricefloors.Country.USA + +@ToString(includeNames = true, ignoreNulls = true) +class RuleEngineModelSchema { + + RuleEngineFunction function + RuleEngineFunctionArgs args + + static RuleEngineModelSchema createDeviceCountryInSchema(List argsCountries = [USA]) { + new RuleEngineModelSchema().tap { + it.function = DEVICE_COUNTRY_IN + it.args = new RuleEngineFunctionArgs(countries: argsCountries) + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy new file mode 100644 index 00000000000..32fd4f22828 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RuleSet.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.config + +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.util.PBSUtils.randomString + +class RuleSet { + + Boolean enabled + Stage stage + String name + String version + List modelGroups + + static RuleSet createRuleSets() { + new RuleSet().tap { + it.enabled = true + it.stage = PROCESSED_AUCTION_REQUEST + it.name = randomString + it.version = randomString + it.modelGroups = [RulesEngineModelGroup.createRulesModuleGroup()] + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy new file mode 100644 index 00000000000..f9361465cd4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/RulesEngineModelGroup.groovy @@ -0,0 +1,29 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.config.RuleEngineModelRule.createRuleEngineModelRule +import static org.prebid.server.functional.model.config.RuleEngineModelSchema.createDeviceCountryInSchema + +class RulesEngineModelGroup { + + Integer weight + String version + String analyticsKey + List schema + @JsonProperty("default") + List modelDefault + List rules + + static RulesEngineModelGroup createRulesModuleGroup() { + new RulesEngineModelGroup().tap { + it.weight = PBSUtils.getRandomNumber(1, 100) + it.version = PBSUtils.randomString + it.analyticsKey = PBSUtils.randomString + it.schema = [createDeviceCountryInSchema()] + it.modelDefault = [] + it.rules = [createRuleEngineModelRule()] + } + } +} 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/Stage.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy index c77fd9ebcda..178f22552ae 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy @@ -6,20 +6,22 @@ import groovy.transform.ToString @ToString enum Stage { - ENTRYPOINT("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") + ENTRYPOINT("entrypoint", "entrypoint"), + RAW_AUCTION_REQUEST("raw-auction-request", "rawauction"), + PROCESSED_AUCTION_REQUEST("processed-auction-request", "procauction"), + BIDDER_REQUEST("bidder-request", "bidrequest"), + RAW_BIDDER_RESPONSE("raw-bidder-response", "rawbidresponse"), + PROCESSED_BIDDER_RESPONSE("processed-bidder-response", "procbidresponse"), + ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses", "allprocbidresponses"), + AUCTION_RESPONSE("auction-response", "auctionresponse") @JsonValue final String value + final String metricValue - Stage(String value) { + Stage(String value, String metricValue) { this.value = value + this.metricValue = metricValue } @Override diff --git a/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy b/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy new file mode 100644 index 00000000000..0e6b8d93119 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/TargetingOrtb.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.User + +@ToString(includeNames = true, ignoreNulls = true) +class TargetingOrtb { + + User user +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy b/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy new file mode 100644 index 00000000000..d9f2bc7b30d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/TargetingResult.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class TargetingResult { + + List audience + TargetingOrtb ortb2 +} 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..5dc72e1b0e8 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.UsNatField 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(UsNatField.SHARING_NOTICE), + SALE_OPT_OUT_NOTICE(UsNatField.SALE_OPT_OUT_NOTICE), + SHARING_OPT_OUT_NOTICE(UsNatField.SHARING_OPT_OUT_NOTICE), + TARGETED_ADVERTISING_OPT_OUT_NOTICE(UsNatField.TARGETED_ADVERTISING_OPT_OUT_NOTICE), + SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE(UsNatField.SENSITIVE_DATA_PROCESSING_OPT_OUT_NOTICE), + SENSITIVE_DATA_LIMIT_USE_NOTICE(UsNatField.SENSITIVE_DATA_LIMIT_USE_NOTICE), + SALE_OPT_OUT(UsNatField.SALE_OPT_OUT), + SHARING_OPT_OUT(UsNatField.SHARING_OPT_OUT), + TARGETED_ADVERTISING_OPT_OUT(UsNatField.TARGETED_ADVERTISING_OPT_OUT), + SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN(UsNatField.SENSITIVE_DATA_PROCESSING + 1), + SENSITIVE_DATA_RELIGIOUS_BELIEFS(UsNatField.SENSITIVE_DATA_PROCESSING + 2), + SENSITIVE_DATA_HEALTH_INFO(UsNatField.SENSITIVE_DATA_PROCESSING + 3), + SENSITIVE_DATA_ORIENTATION(UsNatField.SENSITIVE_DATA_PROCESSING + 4), + SENSITIVE_DATA_CITIZENSHIP_STATUS(UsNatField.SENSITIVE_DATA_PROCESSING + 5), + SENSITIVE_DATA_GENETIC_ID(UsNatField.SENSITIVE_DATA_PROCESSING + 6), + SENSITIVE_DATA_BIOMETRIC_ID(UsNatField.SENSITIVE_DATA_PROCESSING + 7), + SENSITIVE_DATA_GEOLOCATION(UsNatField.SENSITIVE_DATA_PROCESSING + 8), + SENSITIVE_DATA_ID_NUMBERS(UsNatField.SENSITIVE_DATA_PROCESSING + 9), + SENSITIVE_DATA_ACCOUNT_INFO(UsNatField.SENSITIVE_DATA_PROCESSING + 10), + SENSITIVE_DATA_UNION_MEMBERSHIP(UsNatField.SENSITIVE_DATA_PROCESSING + 11), + SENSITIVE_DATA_COMMUNICATION_CONTENTS(UsNatField.SENSITIVE_DATA_PROCESSING + 12), + SENSITIVE_DATA_PROCESSING_ALL(UsNatField.SENSITIVE_DATA_PROCESSING + "*"), + CHILD_CONSENTS_FROM_13_TO_16(UsNatField.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 1), + CHILD_CONSENTS_BELOW_13(UsNatField.KNOWN_CHILD_SENSITIVE_DATA_CONSENTS + 2), + PERSONAL_DATA_CONSENTS(UsNatField.PERSONAL_DATA_CONSENTS), + MSPA_COVERED_TRANSACTION(UsNatField.MSPA_COVERED_TRANSACTION), + MSPA_OPT_OUT_OPTION_MODE(UsNatField.MSPA_OPT_OUT_OPTION_MODE), + MSPA_SERVICE_PROVIDER_MODE(UsNatField.MSPA_SERVICE_PROVIDER_MODE), + GPC(UsNatField.GPC); @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ValueRestrictedRule.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ValueRestrictedRule.groovy index 0e2c0051c7e..913ae586a4c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ValueRestrictedRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ValueRestrictedRule.groovy @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonSerialize import groovy.transform.ToString +import org.prebid.server.functional.model.privacy.gpp.GppDataActivity @ToString(includeNames = true, ignoreNulls = true) @JsonSerialize(using = ValueRestrictedRuleSerializer.class) @@ -13,13 +14,13 @@ import groovy.transform.ToString abstract class ValueRestrictedRule { protected UsNationalPrivacySection privacySection - protected DataActivity value + protected GppDataActivity value protected static final String JSON_LOGIC_VALUE_FIELD = "var" - ValueRestrictedRule(UsNationalPrivacySection privacySection, DataActivity dataActivity) { + ValueRestrictedRule(UsNationalPrivacySection privacySection, GppDataActivity value) { this.privacySection = privacySection - this.value = dataActivity + this.value = value } static class ValueRestrictedRuleSerializer extends JsonSerializer { @@ -30,7 +31,7 @@ abstract class ValueRestrictedRule { jsonGenerator.writeStartObject() jsonGenerator.writeStringField(JSON_LOGIC_VALUE_FIELD, valueRestrictedRule.privacySection.value) jsonGenerator.writeEndObject() - jsonGenerator.writeObject(valueRestrictedRule.value.dataActivityBits) + jsonGenerator.writeObject(valueRestrictedRule.value.value) jsonGenerator.writeEndArray() } } 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/StoredProfileImp.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy new file mode 100644 index 00000000000..19f87ac0d9c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileImp.groovy @@ -0,0 +1,46 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ImpConfigTypeConverter +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.ProfileType + +@Entity +@Table(name = "profiles") +@ToString(includeNames = true) +class StoredProfileImp { + + @Id + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "type") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profile") + @Convert(converter = ImpConfigTypeConverter) + Imp impBody + + static StoredProfileImp getProfile(ImpProfile profile) { + new StoredProfileImp().tap { + it.profileName = profile.id + it.accountId = profile.accountId + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.impBody = profile.body + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy new file mode 100644 index 00000000000..8b04143d368 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/StoredProfileRequest.groovy @@ -0,0 +1,46 @@ +package org.prebid.server.functional.model.db + +import groovy.transform.ToString +import jakarta.persistence.Column +import jakarta.persistence.Convert +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import org.prebid.server.functional.model.db.typeconverter.ProfileMergePrecedenceConvert +import org.prebid.server.functional.model.db.typeconverter.ProfileTypeConvert +import org.prebid.server.functional.model.db.typeconverter.BidRequestConfigTypeConverter +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.RequestProfile +import org.prebid.server.functional.model.request.profile.ProfileType + +@Entity +@Table(name = "profiles") +@ToString(includeNames = true) +class StoredProfileRequest { + + @Id + @Column(name = "profileId") + String profileName + @Column(name = "accountId") + String accountId + @Column(name = "mergePrecedence") + @Convert(converter = ProfileMergePrecedenceConvert) + ProfileMergePrecedence mergePrecedence + @Column(name = "type") + @Convert(converter = ProfileTypeConvert) + ProfileType type + @Column(name = "profile") + @Convert(converter = BidRequestConfigTypeConverter) + BidRequest requestBody + + static StoredProfileRequest getProfile(RequestProfile profile) { + new StoredProfileRequest().tap { + it.profileName = profile.id + it.accountId = profile.accountId + it.mergePrecedence = profile.mergePrecedence + it.type = profile.type + it.requestBody = profile.body + } + } +} 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..5bf47b830fc 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 org.prebid.server.functional.model.db.typeconverter.StoredRequestConfigTypeConverter +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.BidRequestConfigTypeConverter 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") @@ -27,7 +27,7 @@ class StoredRequest { @Column(name = "reqId") String requestId @Column(name = "requestData") - @Convert(converter = StoredRequestConfigTypeConverter) + @Convert(converter = BidRequestConfigTypeConverter) BidRequest requestData static StoredRequest getStoredRequest(AmpRequest ampRequest, BidRequest storedRequest) { 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..ebfc31f3c6d 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.db.typeconverter.BidResponseConfigTypeConverter 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") @@ -29,6 +29,6 @@ class StoredResponse { @Convert(converter = StoredAuctionResponseConfigTypeConverter) SeatBid storedAuctionResponse @Column(name = "storedBidResponse") - @Convert(converter = StoredBidResponseConfigTypeConverter) + @Convert(converter = BidResponseConfigTypeConverter) BidResponse storedBidResponse } 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/BidRequestConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy new file mode 100644 index 00000000000..b3761640226 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidRequestConfigTypeConverter.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.util.ObjectMapperWrapper + +class BidRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { + + @Override + String convertToDatabaseColumn(BidRequest bidRequest) { + bidRequest ? encode(bidRequest) : null + } + + @Override + BidRequest convertToEntityAttribute(String value) { + value ? decode(value, BidRequest) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy new file mode 100644 index 00000000000..789d57e045e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/BidResponseConfigTypeConverter.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.ObjectMapperWrapper + +class BidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { + + @Override + String convertToDatabaseColumn(BidResponse bidResponse) { + bidResponse ? encode(bidResponse) : null + } + + @Override + BidResponse convertToEntityAttribute(String value) { + value ? decode(value, BidResponse) : null + } +} 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/ProfileMergePrecedenceConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy new file mode 100644 index 00000000000..2b347cd9521 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileMergePrecedenceConvert.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence + +class ProfileMergePrecedenceConvert implements AttributeConverter { + + @Override + String convertToDatabaseColumn(ProfileMergePrecedence profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileMergePrecedence convertToEntityAttribute(String value) { + value ? ProfileMergePrecedence.forValue(value) : null + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy new file mode 100644 index 00000000000..5c5565385f1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/ProfileTypeConvert.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.db.typeconverter + +import jakarta.persistence.AttributeConverter +import org.prebid.server.functional.model.request.profile.ProfileType + +class ProfileTypeConvert implements AttributeConverter { + + @Override + String convertToDatabaseColumn(ProfileType profileMergePrecedence) { + profileMergePrecedence?.value + } + + @Override + ProfileType convertToEntityAttribute(String value) { + value ? ProfileType.forValue(value) : null + } +} 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 deleted file mode 100644 index 6a1ddb05da8..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredBidResponseConfigTypeConverter.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.functional.model.db.typeconverter - -import javax.persistence.AttributeConverter -import org.prebid.server.functional.model.response.auction.BidResponse -import org.prebid.server.functional.util.ObjectMapperWrapper - -class StoredBidResponseConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { - - @Override - String convertToDatabaseColumn(BidResponse bidResponse) { - bidResponse ? encode(bidResponse) : null - } - - @Override - BidResponse convertToEntityAttribute(String value) { - value ? decode(value, BidResponse) : null - } -} 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 deleted file mode 100644 index 8668b292d8c..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/db/typeconverter/StoredRequestConfigTypeConverter.groovy +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.functional.model.db.typeconverter - -import javax.persistence.AttributeConverter -import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.util.ObjectMapperWrapper - -class StoredRequestConfigTypeConverter implements AttributeConverter, ObjectMapperWrapper { - - @Override - String convertToDatabaseColumn(BidRequest bidRequest) { - bidRequest ? encode(bidRequest) : null - } - - @Override - BidRequest convertToEntityAttribute(String value) { - value ? decode(value, BidRequest) : null - } -} 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/filesystem/FileSystemAccountsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy new file mode 100644 index 00000000000..6851ece5527 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/filesystem/FileSystemAccountsConfig.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.filesystem + +import groovy.transform.ToString +import org.prebid.server.functional.model.config.AccountConfig + +@ToString(includeNames = true, ignoreNulls = true) +class FileSystemAccountsConfig { + + List accounts +} 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/FloorModelGroup.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/FloorModelGroup.groovy new file mode 100644 index 00000000000..14ffa92bcb0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/FloorModelGroup.groovy @@ -0,0 +1,32 @@ +package org.prebid.server.functional.model.pricefloors + +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 +@ToString(includeNames = true, ignoreNulls = true) +class FloorModelGroup { + + Currency currency + Integer skipRate + String modelVersion + Integer modelWeight + PriceFloorSchema schema + Map values + @JsonProperty("default") + BigDecimal defaultFloor + List noFloorSignalBidders + + static FloorModelGroup getModelGroup() { + new FloorModelGroup( + currency: Currency.USD, + schema: PriceFloorSchema.priceFloorSchema, + values: [(new Rule(mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE) + .getRule([PriceFloorField.MEDIA_TYPE, PriceFloorField.COUNTRY])): PBSUtils.randomFloorValue] + ) + } +} 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 deleted file mode 100644 index d8770197821..00000000000 --- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/ModelGroup.groovy +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.functional.model.pricefloors - -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.util.PBSUtils - -@EqualsAndHashCode -@ToString(includeNames = true, ignoreNulls = true) -class ModelGroup { - - Currency currency - Integer skipRate - String modelVersion - Integer modelWeight - PriceFloorSchema schema - Map values - @JsonProperty("default") - BigDecimal defaultFloor - - static ModelGroup getModelGroup() { - new ModelGroup( - currency: Currency.USD, - schema: PriceFloorSchema.priceFloorSchema, - values: [(new Rule(mediaType: MediaType.MULTIPLE, country: Country.MULTIPLE) - .getRule([PriceFloorField.MEDIA_TYPE, PriceFloorField.COUNTRY])): PBSUtils.randomFloorValue] - ) - } -} 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..1c87c85b794 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 @@ -15,14 +16,16 @@ class PriceFloorData implements ResponseModel { String floorProvider Currency currency Integer skipRate + Integer useFetchDataRate String floorsSchemaVersion Integer modelTimestamp - List modelGroups + List modelGroups + List noFloorSignalBidders static PriceFloorData getPriceFloorData() { new PriceFloorData(floorProvider: PBSUtils.randomString, currency: USD, floorsSchemaVersion: 2, - modelGroups: [ModelGroup.modelGroup]) + modelGroups: [FloorModelGroup.modelGroup]) } } 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..80d779e069d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/Metric.groovy @@ -0,0 +1,52 @@ +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 { + + 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/privacy/gpp/GpcSubsectionType.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/GpcSubsectionType.groovy new file mode 100644 index 00000000000..97b44f7ab36 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/GpcSubsectionType.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.privacy.gpp + +import com.fasterxml.jackson.annotation.JsonValue + +enum GpcSubsectionType { + + CORE(0), + GPC(1) + + @JsonValue + final int value + + GpcSubsectionType(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/GppDataActivity.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/GppDataActivity.groovy new file mode 100644 index 00000000000..2ca99595248 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/GppDataActivity.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.privacy.gpp + +import com.fasterxml.jackson.annotation.JsonValue + +enum GppDataActivity { + + INVALID(-1), + NOT_APPLICABLE(0), + NO_CONSENT(1), + CONSENT(2) + + @JsonValue + final int value + + GppDataActivity(int value) { + this.value = value + } + + static GppDataActivity fromInt(int dataActivityBits) { + values().find { it.value == dataActivityBits } + ?: { throw new IllegalArgumentException("Invalid dataActivityBits: ${dataActivityBits}") } as GppDataActivity + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/MspaMode.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/MspaMode.groovy new file mode 100644 index 00000000000..7d2b5845453 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/MspaMode.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +import com.fasterxml.jackson.annotation.JsonValue + +enum MspaMode { + + NOT_APPLICABLE(0), + YES(1), + NO(2) + + @JsonValue + final int value + + MspaMode(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/Notice.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/Notice.groovy new file mode 100644 index 00000000000..5a251e87d47 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/Notice.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +import com.fasterxml.jackson.annotation.JsonValue + +enum Notice { + + NOT_APPLICABLE(0), + PROVIDED(1), + NOT_PROVIDED(2) + + @JsonValue + final int value + + Notice(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/OptOut.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/OptOut.groovy new file mode 100644 index 00000000000..eb82dd645e9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/OptOut.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +import com.fasterxml.jackson.annotation.JsonValue + +enum OptOut { + + NOT_APPLICABLE(0), + OPTED_OUT(1), + DID_NOT_OPT_OUT(2) + + @JsonValue + final int value + + OptOut(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..8a456a5fb72 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1ChildSensitiveData.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsCaliforniaV1ChildSensitiveData { + + GppDataActivity toSellUnder16 + GppDataActivity toShareUnder16 + + static UsCaliforniaV1ChildSensitiveData getDefault(GppDataActivity childUnder13 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom13to16 = GppDataActivity.NOT_APPLICABLE) { + + new UsCaliforniaV1ChildSensitiveData().tap { + it.toSellUnder16 = childUnder13 + it.toShareUnder16 = childFrom13to16 + } + } + + List getContentList() { + [toShareUnder16, toSellUnder16]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1SensitiveData.groovy new file mode 100644 index 00000000000..3d78e8c4874 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsCaliforniaV1SensitiveData.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsCaliforniaV1SensitiveData { + + GppDataActivity idNumbers + GppDataActivity accountInfo + GppDataActivity geolocation + GppDataActivity racialEthnicOrigin + GppDataActivity communicationContents + GppDataActivity geneticId + GppDataActivity biometricId + GppDataActivity healthInfo + GppDataActivity orientation + + List getContentList() { + [idNumbers, accountInfo, geolocation, racialEthnicOrigin, + communicationContents, geneticId, biometricId, healthInfo, orientation]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..659d514a9f0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1ChildSensitiveData.groovy @@ -0,0 +1,16 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsColoradoV1ChildSensitiveData { + + GppDataActivity childSensitive + + static UsColoradoV1ChildSensitiveData getDefault(GppDataActivity childSensitive = GppDataActivity.NOT_APPLICABLE) { + new UsColoradoV1ChildSensitiveData().tap { + it.childSensitive = childSensitive + } + } + + Integer getContentList() { + this.childSensitive.value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1SensitiveData.groovy new file mode 100644 index 00000000000..59e19d673b9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsColoradoV1SensitiveData.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsColoradoV1SensitiveData { + + GppDataActivity racialEthnicOrigin + GppDataActivity religiousBeliefs + GppDataActivity healthInfo + GppDataActivity orientation + GppDataActivity citizenshipStatus + GppDataActivity geneticId + GppDataActivity biometricId + + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, healthInfo, orientation, + citizenshipStatus, geneticId, biometricId]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..0c9e2eae97c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1ChildSensitiveData.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsConnecticutV1ChildSensitiveData { + + GppDataActivity childUnder13 + GppDataActivity childFrom13to16 + GppDataActivity childFrom13to16Targeted + + static UsConnecticutV1ChildSensitiveData getDefault(GppDataActivity childUnder13 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom13to16 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom16to18 = GppDataActivity.NOT_APPLICABLE) { + + new UsConnecticutV1ChildSensitiveData().tap { + it.childUnder13 = childUnder13 + it.childFrom13to16 = childFrom13to16 + it.childFrom13to16Targeted = childFrom16to18 + } + } + + List getContentList() { + [childUnder13, childFrom13to16, childFrom13to16Targeted]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1SensitiveData.groovy new file mode 100644 index 00000000000..844b435cfe4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsConnecticutV1SensitiveData.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsConnecticutV1SensitiveData { + + GppDataActivity racialEthnicOrigin + GppDataActivity religiousBeliefs + GppDataActivity healthInfo + GppDataActivity orientation + GppDataActivity citizenshipStatus + GppDataActivity geneticId + GppDataActivity biometricId + GppDataActivity geolocation + GppDataActivity idNumbers + + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, healthInfo, orientation, + citizenshipStatus, geneticId, biometricId, geolocation, idNumbers]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..5a1c3fae625 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1ChildSensitiveData.groovy @@ -0,0 +1,20 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsNationalV1ChildSensitiveData { + + GppDataActivity childUnder13 + GppDataActivity childFrom13to16 + + static UsNationalV1ChildSensitiveData getDefault(GppDataActivity childUnder13 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom13to16 = GppDataActivity.NOT_APPLICABLE) { + + new UsNationalV1ChildSensitiveData().tap { + it.childUnder13 = childUnder13 + it.childFrom13to16 = childFrom13to16 + } + } + + List getContentList() { + [childFrom13to16, childUnder13]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1SensitiveData.groovy new file mode 100644 index 00000000000..81a26ea9c4c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV1SensitiveData.groovy @@ -0,0 +1,23 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsNationalV1SensitiveData { + + GppDataActivity racialEthnicOrigin + GppDataActivity religiousBeliefs + GppDataActivity healthInfo + GppDataActivity orientation + GppDataActivity citizenshipStatus + GppDataActivity geneticId + GppDataActivity biometricId + GppDataActivity geolocation + GppDataActivity idNumbers + GppDataActivity accountInfo + GppDataActivity unionMembership + GppDataActivity communicationContents + + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, healthInfo, orientation, + citizenshipStatus, geneticId, biometricId, geolocation, + idNumbers, accountInfo, unionMembership, communicationContents]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2ChildSensitiveData.groovy new file mode 100644 index 00000000000..aaf325b93aa --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2ChildSensitiveData.groovy @@ -0,0 +1,21 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsNationalV2ChildSensitiveData extends UsNationalV1ChildSensitiveData { + + GppDataActivity childFrom16to17 + + static UsNationalV2ChildSensitiveData getDefault(GppDataActivity childUnder13 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom13to16 = GppDataActivity.NOT_APPLICABLE, + GppDataActivity childFrom16to17 = GppDataActivity.NOT_APPLICABLE) { + + new UsNationalV2ChildSensitiveData().tap { + it.childUnder13 = childUnder13 + it.childFrom13to16 = childFrom13to16 + it.childFrom16to17 = childFrom16to17 + } + } + + List getContentList() { + [childFrom13to16, childUnder13, childFrom16to17]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2SensitiveData.groovy new file mode 100644 index 00000000000..e6f17e23d3f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsNationalV2SensitiveData.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsNationalV2SensitiveData extends UsNationalV1SensitiveData { + + GppDataActivity consumerHealthData + GppDataActivity crimeVictim + GppDataActivity nationalOrigin + GppDataActivity transgenderStatus + + @Override + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, healthInfo, orientation, + citizenshipStatus, geneticId, biometricId, geolocation, + idNumbers, accountInfo, unionMembership, communicationContents, + consumerHealthData, crimeVictim, nationalOrigin, transgenderStatus]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..f0557652782 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1ChildSensitiveData.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsUtahV1ChildSensitiveData { + + GppDataActivity childSensitive + + static UsUtahV1ChildSensitiveData getDefault(GppDataActivity childSensitive = GppDataActivity.NOT_APPLICABLE) { + + new UsUtahV1ChildSensitiveData().tap { + it.childSensitive = childSensitive + } + } + + Integer getContentList() { + childSensitive.value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1SensitiveData.groovy new file mode 100644 index 00000000000..53845f9318a --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsUtahV1SensitiveData.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsUtahV1SensitiveData { + + GppDataActivity racialEthnicOrigin + GppDataActivity religiousBeliefs + GppDataActivity orientation + GppDataActivity citizenshipStatus + GppDataActivity healthInfo + GppDataActivity geneticId + GppDataActivity biometricId + GppDataActivity geolocation + + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, orientation, citizenshipStatus, + healthInfo, geneticId, biometricId, geolocation]*.value.collect { it ?: 0 } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1ChildSensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1ChildSensitiveData.groovy new file mode 100644 index 00000000000..4be70d3739d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1ChildSensitiveData.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsVirginiaV1ChildSensitiveData { + + GppDataActivity childSensitive + + static UsVirginiaV1ChildSensitiveData getDefault(GppDataActivity childSensitive = GppDataActivity.NOT_APPLICABLE) { + + new UsVirginiaV1ChildSensitiveData().tap { + it.childSensitive = childSensitive + } + } + + Integer getContentList() { + childSensitive.value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1SensitiveData.groovy b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1SensitiveData.groovy new file mode 100644 index 00000000000..3185883a32b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/privacy/gpp/UsVirginiaV1SensitiveData.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.privacy.gpp + +class UsVirginiaV1SensitiveData { + + GppDataActivity racialEthnicOrigin + GppDataActivity religiousBeliefs + GppDataActivity healthInfo + GppDataActivity orientation + GppDataActivity citizenshipStatus + GppDataActivity geneticId + GppDataActivity biometricId + GppDataActivity geolocation + + List getContentList() { + [racialEthnicOrigin, religiousBeliefs, healthInfo, orientation, + citizenshipStatus, geneticId, biometricId, geolocation]*.value.collect { it ?: 0 } + } +} 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..8f92eb35210 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.UsCa +import com.iab.gpp.encoder.section.UsCo +import com.iab.gpp.encoder.section.UsCt +import com.iab.gpp.encoder.section.UsNat +import com.iab.gpp.encoder.section.UsUt +import com.iab.gpp.encoder.section.UsVa 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(UsNat.ID), + US_CA_V1(UsCa.ID), + US_VA_V1(UsVa.ID), + US_CO_V1(UsCo.ID), + US_UT_V1(UsUt.ID), + US_CT_V1(UsCt.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/AdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy new file mode 100644 index 00000000000..953f66fd988 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.request.auction + +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.Currency + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class AdjustmentRule { + + @JsonProperty('adjtype') + AdjustmentType adjustmentType + BigDecimal value + Currency currency +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy new file mode 100644 index 00000000000..20574d525a1 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum AdjustmentType { + + MULTIPLIER, CPM, STATIC, UNKNOWN + + @JsonValue + String getValue() { + name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Adrino.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Adrino.groovy new file mode 100644 index 00000000000..2d1675194e9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Adrino.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.request.auction + +class Adrino { + + Integer hash +} 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/Amx.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Amx.groovy new file mode 100644 index 00000000000..3ae0dcd17a3 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Amx.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import org.prebid.server.functional.model.bidder.BidderAdapter +import org.prebid.server.functional.model.bidder.BidderName + +class Amx implements BidderAdapter { + + @JsonProperty("ct") + Integer creativeType + @JsonProperty("startdelay") + Integer startDelay + @JsonProperty("ds") + String demandSource + @JsonProperty("bc") + BidderName bidderCode +} 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/AppExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy index b31926c14b5..ee3c1c9a8f0 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy @@ -6,4 +6,5 @@ import groovy.transform.ToString class AppExt { AppExtData data + AppPrebid prebid } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy new file mode 100644 index 00000000000..edb365d4d6f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AppPrebid { + + String source + String version +} 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/AuctionEnvironment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy new file mode 100644 index 00000000000..649c539e794 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AuctionEnvironment.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum AuctionEnvironment { + + NOT_SUPPORTED(0), + DEVICE_ORCHESTRATED(1), + SERVER_ORCHESTRATED(3), + UNKNOWN(Integer.MAX_VALUE), + + @JsonValue + private int value + + AuctionEnvironment(Integer value) { + this.value = value + } +} 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..97461b79d78 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,18 @@ 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 Banner { List format - Integer w - Integer h + @JsonProperty("w") + Integer width + @JsonProperty("h") + Integer height List btype List battr Integer pos diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy new file mode 100644 index 00000000000..7f7250a6a75 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy @@ -0,0 +1,20 @@ +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 +import org.prebid.server.functional.util.PBSUtils + +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustment { + + Map mediaType + Integer version + + static getDefaultWithSingleMediaTypeRule(BidAdjustmentMediaType type, + BidAdjustmentRule rule, + Integer version = PBSUtils.randomNumber) { + new BidAdjustment(mediaType: [(type): rule], version: version) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy index a005d407241..9cb90edb27b 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy @@ -15,7 +15,6 @@ class BidAdjustmentFactors { Map adjustments Map> mediaTypes - @JsonAnyGetter Map getAdjustments() { adjustments diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy index a959f5b800c..26a58655215 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy @@ -8,7 +8,10 @@ enum BidAdjustmentMediaType { AUDIO("audio"), NATIVE("native"), VIDEO("video"), - VIDEO_OUTSTREAM("video-outstream") + VIDEO_IN_STREAM("video-instream"), + VIDEO_OUT_STREAM("video-outstream"), + ANY('*'), + UNKNOWN('unknown') @JsonValue String value diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy new file mode 100644 index 00000000000..92af741601f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -0,0 +1,19 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class BidAdjustmentRule { + + @JsonProperty('*') + Map> wildcardBidder + Map> generic + Map> openx + Map> alias + @JsonProperty("ALIAS") + Map> aliasUpperCase + @JsonProperty("AlIaS") + Map> aliasCamelCase + Map> amx +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index 1157580209a..26e9ddc2057 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -4,11 +4,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.response.auction.MediaType +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED 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.NATIVE import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO @EqualsAndHashCode @@ -22,7 +25,7 @@ class BidRequest { Dooh dooh Device device User user - Integer test + DebugCondition test Integer at Long tmax List wseat @@ -47,10 +50,18 @@ class BidRequest { getDefaultRequest(channel, Imp.getDefaultImpression(VIDEO)) } + static BidRequest getDefaultNativeRequest(DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(NATIVE)) + } + static BidRequest getDefaultAudioRequest(DistributionChannel channel = SITE) { getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO)) } + static BidRequest getDefaultBidRequest(MediaType mediaType, DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(mediaType)) + } + static BidRequest getDefaultStoredRequest() { getDefaultBidRequest().tap { site = null @@ -63,7 +74,7 @@ class BidRequest { regs = Regs.defaultRegs id = UUID.randomUUID() tmax = 2500 - ext = new BidRequestExt(prebid: new Prebid(debug: 1)) + ext = new BidRequestExt(prebid: new Prebid(debug: ENABLED)) if (channel == SITE) { site = Site.defaultSite } 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..3c39de5781e 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 @@ -1,12 +1,17 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.bidder.AppNexus +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) 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/BidRounding.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy new file mode 100644 index 00000000000..ba612ce9f87 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidRounding { + + UP("up"), + DOWN("down"), + TRUE("true"), + TIME_SPLIT("timesplit"), + UNKNOWN("unknown"), + + private String value + + BidRounding(String value) { + this.value = value + } + + @Override + @JsonValue + String toString() { + return value + } +} 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..9c4a17ec5cf 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 @@ -12,13 +12,25 @@ import org.prebid.server.functional.model.bidder.Rubicon class Bidder { Generic alias + @JsonProperty("ALIAS") + Generic aliasUpperCase Generic generic + @JsonProperty("gener_x") + Generic generX @JsonProperty("GeNerIc") Generic genericCamelCase Rubicon rubicon @JsonProperty("appnexus") AppNexus appNexus Openx openx + Ix ix + @JsonProperty("openxalias") + Openx openxAlias + Adrino adrino + Generic nativo + Amx amx + @JsonProperty("AMX") + Amx amxUpperCase 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/ConsentedProvidersSettings.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy new file mode 100644 index 00000000000..aa7bd511cb2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.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 + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) +class ConsentedProvidersSettings { + + String consentedProviders +} 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/DebugCondition.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy new file mode 100644 index 00000000000..066080c56da --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy @@ -0,0 +1,15 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DebugCondition { + + DISABLED(0), ENABLED(1) + + @JsonValue + final int value + + private DebugCondition(int value) { + this.value = value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy index fbce486ac47..60ef8fa7884 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Device.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.request.auction import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils @ToString(includeNames = true, ignoreNulls = true) class Device { @@ -12,7 +13,7 @@ class Device { UserAgent sua String ip String ipv6 - Integer devicetype + DeviceType devicetype String make String model String os @@ -38,4 +39,16 @@ class Device { String macsha1 String macmd5 DeviceExt ext + + static Device getDefault() { + new Device().tap { + didsha1 = PBSUtils.randomString + didmd5 = PBSUtils.randomString + dpidsha1 = PBSUtils.randomString + ifa = PBSUtils.randomString + macsha1 = PBSUtils.randomString + macmd5 = PBSUtils.randomString + dpidmd5 = PBSUtils.randomString + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy new file mode 100644 index 00000000000..b33855723d7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DeviceType.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum DeviceType { + + MOBILE_TABLET_GENERAL(1), + PERSONAL_COMPUTER(2), + CONNECTED_TV(3), + PHONE(4), + TABLET(5), + CONNECTED_DEVICE(6), + SET_TOP_BOX(7), + OOH_DEVICE(8) + + @JsonValue + final Integer value + + DeviceType(Integer value) { + this.value = value + } +} 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..25e14e62126 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,28 @@ 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 + } + } + + static Eid from(EidPermission eidPermission, List uids = [Uid.defaultUid]) { + new Eid().tap { + it.source = eidPermission.source + it.uids = uids + it.inserter = eidPermission.inserter + it.matcher = eidPermission.matcher + it.matchMethod = eidPermission.matchMethod } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/EidPermission.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/EidPermission.groovy index df77f8f1fbe..5807c6f9241 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/EidPermission.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/EidPermission.groovy @@ -1,11 +1,39 @@ 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.util.PBSUtils + +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @ToString(includeNames = true, ignoreNulls = true) class EidPermission { String source - List bidders + String inserter + String matcher + @JsonProperty("mm") + Integer matchMethod + List bidders = [GENERIC] + + static EidPermission getDefaultEidPermission(List bidders = [GENERIC]) { + new EidPermission().tap { + it.source = PBSUtils.randomString + it.inserter = PBSUtils.randomString + it.matcher = PBSUtils.randomString + it.matchMethod = PBSUtils.randomNumber + it.bidders = bidders + } + } + + static EidPermission from(Eid eid, List bidders = [GENERIC]) { + new EidPermission().tap { + it.source = eid.source + it.inserter = eid.inserter + it.matcher = eid.matcher + it.matchMethod = eid.matchMethod + it.bidders = bidders + } + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy index cb5abf3ff87..c0c80038f5c 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy @@ -20,6 +20,7 @@ class ExtPrebidFloors { ExtPrebidPriceFloorEnforcement enforcement Integer skipRate PriceFloorData data + Long maxSchemaDims static ExtPrebidFloors getExtPrebidFloors() { new ExtPrebidFloors(floorMin: FLOOR_MIN, 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..6eec49cf39d 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, SKIPPED, RUN @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..67215776579 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,36 @@ 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 +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class Format { - Integer w - Integer h - Integer wratio - Integer hratio - Integer wmin + @JsonProperty("w") + Integer width + @JsonProperty("h") + Integer height + @JsonProperty("wratio") + Integer widthRatio + @JsonProperty("hratio") + Integer heightRatio + @JsonProperty("wmin") + Integer widthMin static Format getDefaultFormat() { new Format().tap { - w = 300 - h = 250 + width = 300 + height = 250 + } + } + + static Format getRandomFormat() { + new Format().tap { + width = PBSUtils.randomNumber + height = PBSUtils.randomNumber } } } 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..13c97a36ba4 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 @@ -28,12 +30,12 @@ class Imp { Pmp pmp String displayManager String displayManagerVer - Integer instl + OperationState instl String tagId 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..a9d72718e9a 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,15 @@ class ImpExt { ImpExtContextData data String tid String gpid - Integer ae + String sid + @JsonProperty("ae") + AuctionEnvironment auctionEnvironment + String all + String skadn + String general + @JsonProperty("igs") + InterestGroupAuctionSupport interestGroupAuctionSupports + 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..a20e3ea894d 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,11 @@ class ImpExtPrebid { Bidder bidder ImpExtPrebidFloors floors Map passThrough + Map imp + String adUnitCode + PrebidOptions options + @JsonProperty("profiles") + List profileNames static ImpExtPrebid getDefaultImpExtPrebid() { new ImpExtPrebid().tap { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy new file mode 100644 index 00000000000..15f737d317e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ImpUnitCode.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.request.auction + +enum ImpUnitCode { + + TAG_ID, + GPID, + PB_AD_SLOT, + STORED_REQUEST +} + diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/InterestGroupAuctionSupport.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/InterestGroupAuctionSupport.groovy new file mode 100644 index 00000000000..31d3628587d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/InterestGroupAuctionSupport.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonProperty +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class InterestGroupAuctionSupport { + + @JsonProperty("ae") + AuctionEnvironment auctionEnvironment +} 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..d68075b6668 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/IxDiag.groovy @@ -0,0 +1,12 @@ +package org.prebid.server.functional.model.request.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class IxDiag { + + String pbsv + String pbsp + String pbjsv + String multipleSiteIds +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PaaFormat.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PaaFormat.groovy new file mode 100644 index 00000000000..79f41e953fe --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PaaFormat.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum PaaFormat { + + ORIGINAL, IAB, INVALID + + @JsonValue + String getValue() { + name().toLowerCase() + } +} 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..23b4e7f87a5 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 @@ -1,20 +1,23 @@ package org.prebid.server.functional.model.request.auction +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.ChannelType import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.config.AlternateBidderCodes @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class Prebid { - Integer debug + DebugCondition debug Boolean returnAllBidStatus Map aliases Map aliasgvlids BidAdjustmentFactors bidAdjustmentFactors + BidAdjustment bidAdjustments PrebidCurrency currency Targeting targeting TraceLevel trace @@ -28,7 +31,7 @@ class Prebid { List multibid Pbs pbs Server server - Map> bidderParams + Map bidderParams ExtPrebidFloors floors Map passThrough Events events @@ -37,6 +40,15 @@ class Prebid { List adServerTargeting BidderControls bidderControls PrebidModulesConfig modules + PrebidAnalytics analytics + StoredAuctionResponse storedAuctionResponse + PaaFormat paaFormat + @JsonProperty("alternatebiddercodes") + AlternateBidderCodes alternateBidderCodes + @JsonProperty("profiles") + List profileNames + @JsonProperty("kvps") + Map keyValuePairs 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/PrebidOptions.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidOptions.groovy new file mode 100644 index 00000000000..3763eb668aa --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidOptions.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 + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class PrebidOptions { + + Boolean echoVideoAttrs +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidStoredRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidStoredRequest.groovy index 9f55008332f..024fa9dfac5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidStoredRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PrebidStoredRequest.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true) +@EqualsAndHashCode class PrebidStoredRequest { String id 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..9bdb94f007d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy @@ -0,0 +1,17 @@ +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"), + BGR_IP("31.211.128.0", "2002:1fd3:8000:0000:0000:0000:0000:0000") + + 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..d7e9cb2242e 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,10 @@ import groovy.transform.ToString @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class RegsExt { + @Deprecated(since = "enabling support of ortb 2.6") Integer gdpr + Integer coppa + @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/Site.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy index b74d83ff8fb..c8dfcbdbe79 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Site.groovy @@ -2,9 +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 @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class Site { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy index 87b5e19c21c..7e8ecd556fa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/SiteExtData.groovy @@ -1,8 +1,10 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.util.PBSUtils +@EqualsAndHashCode @ToString(includeNames = true, ignoreNulls = true) class SiteExtData { 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/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy index e547d8f37ed..af07e197c28 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy @@ -1,8 +1,12 @@ package org.prebid.server.functional.model.request.auction +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.SnakeCaseStrategy) class UserExt { String consent @@ -11,6 +15,9 @@ class UserExt { UserTime time UserExtData data UserExtPrebid prebid + ConsentedProvidersSettings consentedProvidersSettings + @JsonProperty("ConsentedProvidersSettings") + ConsentedProvidersSettings consentedProvidersSettingsCamelCase static UserExt getFPDUserExt() { new UserExt(data: UserExtData.FPDUserExtData) 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..53e86c5ed28 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 width + @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"], width: 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..054cc76663b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/cache/BidCachePut.groovy @@ -0,0 +1,18 @@ +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 + String key +} 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/request/profile/ImpProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy new file mode 100644 index 00000000000..aa63358fb24 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ImpProfile.groovy @@ -0,0 +1,26 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class ImpProfile extends Profile { + + static ImpProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), + Imp imp = Imp.defaultImpression, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new ImpProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.IMP + it.mergePrecedence = mergePrecedence + it.body = imp + it.accountId = accountId + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy new file mode 100644 index 00000000000..2bb6387da77 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/Profile.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty + +abstract class Profile { + + @JsonIgnore + String accountId + @JsonIgnore + String id + ProfileType type + @JsonProperty("mergeprecedence") + ProfileMergePrecedence mergePrecedence + T body + + String getRecordName() { + "${accountId}-${id}" + } + + String getFileName() { + "${recordName}.json" + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy new file mode 100644 index 00000000000..80227015989 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileMergePrecedence.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileMergePrecedence { + + EMPTY(""), + REQUEST("request"), + PROFILE("profile"), + UNKNOWN("unknown") + + private final String value + + ProfileMergePrecedence(String value) { + this.value = value + } + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileMergePrecedence forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy new file mode 100644 index 00000000000..6254976300f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/ProfileType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.request.profile + +import com.fasterxml.jackson.annotation.JsonValue +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +enum ProfileType { + + EMPTY(""), + REQUEST("request"), + IMP("imp"), + UNKNOWN("unknown") + + private final String value + + ProfileType(String value) { + this.value = value + } + + @JsonValue + String getValue() { + name().toLowerCase() + } + + static ProfileType forValue(String value) { + values().find { it.value == value } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy new file mode 100644 index 00000000000..62d36bddc38 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/profile/RequestProfile.groovy @@ -0,0 +1,40 @@ +package org.prebid.server.functional.model.request.profile + +import groovy.transform.ToString +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.Site +import org.prebid.server.functional.util.PBSUtils + +import static ProfileMergePrecedence.PROFILE + +@ToString(includeNames = true, ignoreNulls = true) +class RequestProfile extends Profile { + + static RequestProfile getProfile(String accountId = PBSUtils.randomNumber.toString(), + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + BidRequest request = BidRequest.defaultBidRequest.tap { + it.id = null + it.imp = null + it.site = Site.configFPDSite + it.device = Device.default + } + getProfile(accountId, request, name, mergePrecedence) + } + + static RequestProfile getProfile(String accountId, + BidRequest request, + String name = PBSUtils.randomString, + ProfileMergePrecedence mergePrecedence = PROFILE) { + + new RequestProfile().tap { + it.accountId = accountId + it.id = name + it.type = ProfileType.REQUEST + it.mergePrecedence = mergePrecedence + it.body = request + it.accountId = accountId + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy index a40623d1f27..49553ded1ff 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/SetuidRequest.groovy @@ -23,9 +23,9 @@ class SetuidRequest { String account static SetuidRequest getDefaultSetuidRequest() { - def request = new SetuidRequest() - request.bidder = GENERIC - request.gdpr = "0" - request + new SetuidRequest().tap { + bidder = GENERIC + gdpr = "0" + } } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy index 2d9d7ee7d36..146f2724325 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/setuid/UidWithExpiry.groovy @@ -11,10 +11,10 @@ class UidWithExpiry { String uid ZonedDateTime expires - static UidWithExpiry getDefaultUidWithExpiry() { + static UidWithExpiry getDefaultUidWithExpiry(Integer daysUntilExpiry = 2) { new UidWithExpiry().tap { uid = UUID.randomUUID().toString() - expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(2) + expires = ZonedDateTime.now(Clock.systemUTC()).plusDays(daysUntilExpiry) } } } 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/amp/RawAmpResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy index 9d037af80ff..18e7f705a1e 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/amp/RawAmpResponse.groovy @@ -6,5 +6,5 @@ import groovy.transform.ToString class RawAmpResponse { String responseBody - Map headers + Map> headers } 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..0d1a3ba16ad 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) @@ -17,7 +17,7 @@ class ActivityInfrastructure { RuleConfiguration ruleConfiguration Boolean allowByDefault Boolean allowed - String result + RuleResult result String region String country } 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/AnalyticResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy index 7976ae24c2f..a8b737a848a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/AnalyticResult.groovy @@ -8,6 +8,7 @@ import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Imp import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS_BLOCK @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @@ -20,7 +21,7 @@ class AnalyticResult { static AnalyticResult buildFromImp(Imp imp) { def appliedTo = new AppliedTo(impIds: [imp.id], bidders: [imp.ext.prebid.bidder.configuredBidders.first()]) - def impResult = new ImpResult(status: 'success-block', values: new ModuleValue(richmediaFormat: 'mraid'), appliedTo: appliedTo) + def impResult = new ImpResult(status: SUCCESS_BLOCK, values: new ModuleValue(richmediaFormat: 'mraid'), appliedTo: appliedTo) new AnalyticResult(name: 'reject-richmedia', status: SUCCESS, results: [impResult]) } } 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/And.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy index 4072d3e1f62..73799db84d5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/And.groovy @@ -1,11 +1,18 @@ 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.PrivacyModule @ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy) class And { List and + PrivacyModule privacyModule + Boolean skipped + RuleResult result } 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..49beb80792f 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,12 +1,17 @@ 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 import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils +import static groovy.lang.Closure.DELEGATE_FIRST + @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Bid implements ObjectMapperWrapper { String id @@ -29,17 +34,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 width + @JsonProperty("h") + Integer height + @JsonProperty("wratio") + Integer widthRatio + @JsonProperty("hratio") + Integer heightRatio Integer exp Integer dur - Integer mtype + @JsonProperty("mtype") + BidMediaType mediaType Integer slotinpod BidExt ext @@ -51,16 +62,33 @@ class Bid implements ObjectMapperWrapper { new Bid().tap { id = UUID.randomUUID() impid = imp.id - price = PBSUtils.getRandomPrice() + price = imp.bidFloor != null ? imp.bidFloor : 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 + width = imp.banner && imp.banner.format ? imp.banner.format.first().width : null if (imp.nativeObj || imp.video) { adm = new Adm(assets: [Asset.defaultAsset]) } } } + static List getDefaultMultiTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { + List bids = [] + if (imp.banner) bids << createBid(imp, BidMediaType.BANNER) { adm = null } + if (imp.video) bids << createBid(imp, BidMediaType.VIDEO) + if (imp.nativeObj) bids << createBid(imp, BidMediaType.NATIVE) + if (imp.audio) bids << createBid(imp, BidMediaType.AUDIO) { adm = null } + + if (commonInit) { + bids.each { bid -> + commonInit.delegate = bid + commonInit.resolveStrategy = DELEGATE_FIRST + commonInit() + } + } + bids + } + void setAdm(Object adm) { if (adm instanceof Adm) { this.adm = encode(adm) @@ -70,4 +98,15 @@ class Bid implements ObjectMapperWrapper { this.adm = null } } + + private static Bid createBid(Imp imp, BidMediaType type, @DelegatesTo(Bid) Closure init = null) { + def bid = getDefaultBid(imp) + bid.mediaType = type + if (init) { + init.delegate = bid + init.resolveStrategy = DELEGATE_FIRST + init() + } + bid + } } 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..58d2e2178e3 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 @@ -1,7 +1,9 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) class BidExt { @@ -9,5 +11,13 @@ class BidExt { Prebid prebid BigDecimal origbidcpm Currency origbidcur - Dsa dsa + DsaResponse dsa + @JsonProperty("ct") + Integer creativeType + @JsonProperty("startdelay") + Integer startDelay + @JsonProperty("ds") + String demandSource + @JsonProperty("bc") + BidderName bidderCode } 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..84b5a5d9953 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -0,0 +1,31 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType + +enum BidMediaType { + + BANNER(1), + VIDEO(2), + AUDIO(3), + NATIVE(4) + + @JsonValue + final Integer value + + BidMediaType(Integer value) { + this.value = value + } + + static BidMediaType from(BidAdjustmentMediaType mediaType) { + return switch (mediaType) { + case BidAdjustmentMediaType.BANNER -> BANNER + case BidAdjustmentMediaType.VIDEO -> VIDEO + case BidAdjustmentMediaType.VIDEO_IN_STREAM -> VIDEO + case BidAdjustmentMediaType.VIDEO_OUT_STREAM -> VIDEO + case BidAdjustmentMediaType.AUDIO -> AUDIO + case BidAdjustmentMediaType.NATIVE -> NATIVE + default -> throw new IllegalArgumentException("Unknown media type: " + mediaType); + }; + } +} 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..98b8dafb43e 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,27 @@ 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), + ERROR_REQUEST(104), + + REQUEST_BLOCKED_GENERAL(200), + REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201), + REQUEST_BLOCKED_UNSUPPORTED_MEDIA_TYPE(202), + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE(203), + 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..353f89c454e 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 @@ -1,11 +1,11 @@ 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.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 +19,8 @@ class BidResponse implements ResponseModel { String bidid Currency cur String customdata - Integer nbr + @JsonProperty("nbr") + NoBidResponse noBidResponse BidResponseExt ext static BidResponse getDefaultBidResponse(BidRequest bidRequest, BidderName bidderName = GENERIC) { @@ -29,16 +30,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/BidResponseExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponseExt.groovy index 3ccb3384e89..d1d282d663a 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponseExt.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidResponseExt.groovy @@ -1,5 +1,6 @@ package org.prebid.server.functional.model.response.auction +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.ToString import org.prebid.server.functional.model.response.BidderError import org.prebid.server.functional.model.response.Debug @@ -15,4 +16,6 @@ class BidResponseExt { Map usersync BidResponsePrebid prebid Map> warnings + @JsonProperty("igi") + List interestGroupAuctionIntent } 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..80f504a05ec 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,11 @@ enum ErrorType { PREBID("prebid"), CACHE("cache"), ALIAS("alias"), - TARGETING("targeting") + TARGETING("targeting"), + IX("ix"), + OPENX("openx"), + AMX("amx"), + AMX_UPPER_CASE("AMX"), @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/ImpResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy index de157de7775..8740e415f47 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ImpResult.groovy @@ -4,13 +4,14 @@ 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.FetchStatus @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @EqualsAndHashCode class ImpResult { - String status + FetchStatus status ModuleValue values AppliedTo appliedTo } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyer.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyer.groovy new file mode 100644 index 00000000000..b8d1bf80d38 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyer.groovy @@ -0,0 +1,17 @@ +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.Currency + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) +class InterestGroupAuctionBuyer { + + String origin + BigDecimal maxBid + Currency cur + Map pbs + InterestGroupAuctionBuyerExt ext +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyerExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyerExt.groovy new file mode 100644 index 00000000000..4404491246c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionBuyerExt.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.response.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class InterestGroupAuctionBuyerExt { + + String bidder + String adapter +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntent.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntent.groovy new file mode 100644 index 00000000000..29ff15d4eeb --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntent.groovy @@ -0,0 +1,18 @@ +package org.prebid.server.functional.model.response.auction + +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.LowerCaseStrategy) +class InterestGroupAuctionIntent { + + String impId + @JsonProperty("igb") + List interestGroupAuctionBuyer + @JsonProperty("igs") + List interestGroupAuctionSeller + InterestGroupAuctionIntentExt ext +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntentExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntentExt.groovy new file mode 100644 index 00000000000..29aeaf980da --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionIntentExt.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.model.response.auction + +import groovy.transform.ToString +import org.prebid.server.functional.model.bidder.BidderName + +@ToString(includeNames = true, ignoreNulls = true) +class InterestGroupAuctionIntentExt { + + BidderName bidder + BidderName adapter +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSeller.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSeller.groovy new file mode 100644 index 00000000000..192f5c2f611 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSeller.groovy @@ -0,0 +1,14 @@ +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 InterestGroupAuctionSeller { + + String impId + Map config + InterestGroupAuctionSellerExt ext +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSellerExt.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSellerExt.groovy new file mode 100644 index 00000000000..fcecf1b5480 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InterestGroupAuctionSellerExt.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.response.auction + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class InterestGroupAuctionSellerExt { + + String bidder + String adapter +} 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..c5c1a828f98 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,6 @@ class InvocationResult { InvocationStatus status ResponseAction action HookId hookId - AnalyticStags analyticStags + AnalyticsPrebidTag analyticsTags + String message } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy index 257b6287fcf..77c7ffd5ef8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy @@ -6,7 +6,7 @@ import groovy.transform.ToString @ToString enum InvocationStatus { - SUCCESS, FAILURE + SUCCESS, FAILURE, INVOCATION_FAILURE @JsonValue String getValue() { 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/Meta.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Meta.groovy index d022daf9646..e8bb869ebae 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Meta.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Meta.groovy @@ -1,12 +1,15 @@ package org.prebid.server.functional.model.response.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.auction.RendererData @ToString(includeNames = true, ignoreNulls = true) class Meta { - String adapterCode + @JsonProperty("adaptercode") + BidderName adapterCode List advertiserDomains Integer advertiserId String advertiserName 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..8711bd395c6 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy @@ -0,0 +1,17 @@ +package org.prebid.server.functional.model.response.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum ModuleActivityName { + + ORTB2_BLOCKING('enforce-blocking'), + REJECT_RICHMEDIA('reject-richmedia'), + AB_TESTING('core-module-abtests') + + @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/ModuleValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy index e76e8fb3f54..c780efaa422 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy @@ -1,14 +1,24 @@ package org.prebid.server.functional.model.response.auction -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming +import com.fasterxml.jackson.annotation.JsonProperty import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) -@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @EqualsAndHashCode class ModuleValue { - String richmediaFormat + ModuleName module + @JsonProperty("richmedia-format") + String richmediaFormat + String analyticsKey + String analyticsValue + String modelVersion + String conditionFired + String resultFunction + List biddersRemoved + BidRejectionReason seatNonBid + String message } 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/OpenxBidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/OpenxBidResponse.groovy index 2ba60b226c1..b5d5289d5a6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/OpenxBidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/OpenxBidResponse.groovy @@ -1,6 +1,5 @@ package org.prebid.server.functional.model.response.auction - import groovy.transform.ToString import org.prebid.server.functional.model.request.auction.BidRequest diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy index e894510811c..d8accbf82ac 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy @@ -2,6 +2,7 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming +import org.prebid.server.functional.model.request.auction.Video @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class Prebid { @@ -13,4 +14,6 @@ class Prebid { Events events Meta meta Map passThrough + Video storedRequestAttributes + Integer rank } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy index a34cc10ddc3..e9f8f01730f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/RawAuctionResponse.groovy @@ -7,5 +7,5 @@ import org.prebid.server.functional.model.ResponseModel class RawAuctionResponse implements ResponseModel { String responseBody - Map headers + Map> headers } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy index 1a786670ba8..fbcfa74683d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue enum ResponseAction { - UPDATE, NO_ACTION + UPDATE, NO_ACTION, NO_INVOCATION, REJECT @JsonValue String getValue() { 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/model/response/auction/RuleResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleResult.groovy new file mode 100644 index 00000000000..62b4256bb66 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/RuleResult.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.response.auction + +enum RuleResult { + + ALLOW, DISALLOW, ABSTAIN +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy index 16e1fd46459..a30917a18a6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/SeatNonBid.groovy @@ -3,11 +3,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 +import org.prebid.server.functional.model.bidder.BidderName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class SeatNonBid { - String seat + BidderName seat List nonBid } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy index f1a72a9e266..0f155bf55a1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy @@ -3,13 +3,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 -import org.prebid.server.functional.model.config.Stage @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class TraceOutcome { - Stage entity + String entity Long executionTimeMillis List groups } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy b/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy index 140f25c410f..3b6df42c7de 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/biddersparams/BidderParams.groovy @@ -17,4 +17,5 @@ class BidderParams { def appid def placementid def dependencies + def additionalProperties } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy index af240fa0b26..3854da82dde 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/cookiesync/RawCookieSyncResponse.groovy @@ -6,5 +6,5 @@ import groovy.transform.ToString class RawCookieSyncResponse { String responseBody - Map headers + Map> headers } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResponse.groovy new file mode 100644 index 00000000000..c2fc5b0c6c2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResponse.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.response.influx + +class InfluxResponse { + + List results +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResult.groovy new file mode 100644 index 00000000000..ae6f6864334 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/influx/InfluxResult.groovy @@ -0,0 +1,10 @@ +package org.prebid.server.functional.model.response.influx + +import com.fasterxml.jackson.annotation.JsonProperty + +class InfluxResult { + + @JsonProperty("statement_id") + Integer statementId + List series +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/influx/Series.groovy b/src/test/groovy/org/prebid/server/functional/model/response/influx/Series.groovy new file mode 100644 index 00000000000..30b4cf86e61 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/influx/Series.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.response.influx + +class Series { + + String name + Tags tags + List columns + List> values +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/influx/Tags.groovy b/src/test/groovy/org/prebid/server/functional/model/response/influx/Tags.groovy new file mode 100644 index 00000000000..a88807ad8b4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/influx/Tags.groovy @@ -0,0 +1,6 @@ +package org.prebid.server.functional.model.response.influx + +class Tags { + + String measurement +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy index bc35cd07d82..08a9adbb8fb 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/setuid/SetuidResponse.groovy @@ -6,7 +6,7 @@ import org.prebid.server.functional.model.UidsCookie @ToString(includeNames = true, ignoreNulls = true) class SetuidResponse { - Map headers + Map> headers UidsCookie uidsCookie Byte[] responseBody } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/vtrack/TransferValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/vtrack/TransferValue.groovy new file mode 100644 index 00000000000..1a7f24177dd --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/response/vtrack/TransferValue.groovy @@ -0,0 +1,22 @@ +package org.prebid.server.functional.model.response.vtrack + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import org.prebid.server.functional.util.PBSUtils + +@ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode +class TransferValue { + + String adm + Integer width + Integer height + + static final TransferValue getTransferValue(){ + return new TransferValue().tap { + adm = PBSUtils.randomString + width = 300 + height = 250 + } + } +} 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..cd1b9706f79 100644 --- a/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy +++ b/src/test/groovy/org/prebid/server/functional/repository/HibernateRepositoryService.groovy @@ -3,41 +3,59 @@ package org.prebid.server.functional.repository import org.hibernate.SessionFactory import org.hibernate.cfg.Configuration import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest 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.repository.dao.AccountDao +import org.prebid.server.functional.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao 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 + ProfileImpDao profileImpDao + ProfileRequestDao profileRequestDao - 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) storedImpDao = new StoredImpDao(entityManagerUtil) storedRequestDao = new StoredRequestDao(entityManagerUtil) storedResponseDao = new StoredResponseDao(entityManagerUtil) + profileImpDao = new ProfileImpDao(entityManagerUtil) + profileRequestDao = new ProfileRequestDao(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) @@ -49,6 +67,8 @@ class HibernateRepositoryService { configuration.addAnnotatedClass(StoredImp) configuration.addAnnotatedClass(StoredRequest) configuration.addAnnotatedClass(StoredResponse) + configuration.addAnnotatedClass(StoredProfileImp) + configuration.addAnnotatedClass(StoredProfileRequest) SessionFactory sessionFactory = configuration.addProperties(properties).buildSessionFactory() sessionFactory @@ -59,5 +79,7 @@ class HibernateRepositoryService { storedImpDao.removeAll() storedRequestDao.removeAll() storedResponseDao.removeAll() + profileImpDao.removeAll() + profileRequestDao.removeAll() } } diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy new file mode 100644 index 00000000000..00531ff9a7b --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileImpDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileImpDao extends EntityDao { + + ProfileImpDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileImp) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy new file mode 100644 index 00000000000..455c64a3fd8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/repository/dao/ProfileRequestDao.groovy @@ -0,0 +1,11 @@ +package org.prebid.server.functional.repository.dao + +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.repository.EntityManagerUtil + +class ProfileRequestDao extends EntityDao { + + ProfileRequestDao(EntityManagerUtil entityManagerUtil) { + super(entityManagerUtil, StoredProfileRequest) + } +} 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..249d6fa3f13 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 @@ -28,11 +25,14 @@ import org.prebid.server.functional.model.response.cookiesync.CookieSyncResponse import org.prebid.server.functional.model.response.cookiesync.RawCookieSyncResponse import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse import org.prebid.server.functional.model.response.getuids.GetuidResponse +import org.prebid.server.functional.model.response.influx.InfluxResponse import org.prebid.server.functional.model.response.infobidders.BidderInfoResponse import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.model.response.status.StatusResponse +import org.prebid.server.functional.model.response.vtrack.TransferValue import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.ObjectMapperWrapper +import org.prebid.server.functional.util.PBSUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -43,6 +43,8 @@ import java.time.format.DateTimeFormatter import static io.restassured.RestAssured.given import static java.time.ZoneOffset.UTC +import static org.prebid.server.functional.testcontainers.Dependencies.influxdbContainer + class PrebidServerService implements ObjectMapperWrapper { @@ -59,13 +61,13 @@ 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 INFLUX_DB_ENDPOINT = "/query" static final String UIDS_COOKIE_NAME = "uids" private final PrebidServerContainer pbsContainer private final RequestSpecification requestSpecification + private final RequestSpecification influxRequestSpecification private final RequestSpecification adminRequestSpecification private final RequestSpecification prometheusRequestSpecification @@ -77,12 +79,13 @@ class PrebidServerService implements ObjectMapperWrapper { authenticationScheme.password = pbsContainer.ADMIN_ENDPOINT_PASSWORD this.pbsContainer = pbsContainer requestSpecification = new RequestSpecBuilder().setBaseUri(pbsContainer.rootUri) - .build() + .build() + influxRequestSpecification = new RequestSpecBuilder().setBaseUri(pbsContainer.influxUri) + .build() adminRequestSpecification = buildAndGetRequestSpecification(pbsContainer.adminRootUri, authenticationScheme) prometheusRequestSpecification = buildAndGetRequestSpecification(pbsContainer.prometheusRootUri, authenticationScheme) } - @Step("[POST] /openrtb2/auction") BidResponse sendAuctionRequest(BidRequest bidRequest, Map headers = [:]) { def response = postAuction(bidRequest, headers) @@ -90,7 +93,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 +102,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 +116,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 +125,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST] /cookie_sync without cookie") CookieSyncResponse sendCookieSyncRequest(CookieSyncRequest request) { def response = postCookieSync(request) @@ -126,7 +132,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 +139,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 +146,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 +155,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 +164,6 @@ class PrebidServerService implements ObjectMapperWrapper { } } - @Step("[POST RAW] /cookie_sync with uids and additional cookies") RawCookieSyncResponse sendCookieSyncRequestRaw(CookieSyncRequest request, UidsCookie uidsCookie, Map additionalCookies) { @@ -174,14 +175,22 @@ 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) - def response = given(requestSpecification).cookie(UIDS_COOKIE_NAME, uidsCookieAsEncodedJson) - .queryParams(toMap(request)) - .headers(header) - .get(SET_UID_ENDPOINT) + sendSetUidRequest(request, [uidsCookie], header) + } + + SetuidResponse sendSetUidRequest(SetuidRequest request, List uidsCookies, Map header = [:]) { + def cookies = uidsCookies.withIndex().collectEntries { group, index -> + def uidsCookieAsJson = encode(group) + def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) + ["${UIDS_COOKIE_NAME}${index > 0 ? index + 1 : ''}": uidsCookieAsEncodedJson] + } + + def response = given(requestSpecification) + .cookies(cookies) + .queryParams(toMap(request)) + .headers(header) + .get(SET_UID_ENDPOINT) checkResponseStatusCode(response) @@ -192,39 +201,44 @@ class PrebidServerService implements ObjectMapperWrapper { setuidResponse } - @Step("[GET] /getuids") GetuidResponse sendGetUidRequest(UidsCookie uidsCookie) { def uidsCookieAsJson = encode(uidsCookie) def uidsCookieAsEncodedJson = Base64.urlEncoder.encodeToString(uidsCookieAsJson.bytes) def response = given(requestSpecification).cookie(UIDS_COOKIE_NAME, uidsCookieAsEncodedJson) - .get(GET_UIDS_ENDPOINT) + .get(GET_UIDS_ENDPOINT) checkResponseStatusCode(response) decode(response.body.asString(), GetuidResponse) } - @Step("[GET] /event") byte[] sendEventRequest(EventRequest eventRequest, Map headers = [:]) { def response = given(requestSpecification).headers(headers) - .queryParams(toMap(eventRequest)) - .get(EVENT_ENDPOINT) + .queryParams(toMap(eventRequest)) + .get(EVENT_ENDPOINT) checkResponseStatusCode(response) response.body.asByteArray() } - @Step("[POST] /vtrack") - PrebidCacheResponse sendVtrackRequest(VtrackRequest request, String account) { - def response = given(requestSpecification).queryParam("a", account) - .body(request) - .post(VTRACK_ENDPOINT) + PrebidCacheResponse sendPostVtrackRequest(VtrackRequest request, String account) { + def response = given(requestSpecification).queryParams(["a": account]) + .body(request) + .post(VTRACK_ENDPOINT) checkResponseStatusCode(response) decode(response.body.asString(), PrebidCacheResponse) } - @Step("[GET] /status") + TransferValue sendGetVtrackRequest(Map parameters) { + def response = given(requestSpecification) + .queryParams(parameters) + .get(VTRACK_ENDPOINT) + + checkResponseStatusCode(response) + decode(response.body.asString(), TransferValue) + } + StatusResponse sendStatusRequest() { def response = given(requestSpecification).get(STATUS_ENDPOINT) @@ -232,7 +246,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 +253,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 +260,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 +268,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 +275,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,16 +282,14 @@ 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) + .get(HTTP_INTERACTION_ENDPOINT) checkResponseStatusCode(response) response.body().asString() } - @Step("[GET] /collected-metrics") Map sendCollectedMetricsRequest() { def response = given(adminRequestSpecification).get(COLLECTED_METRICS_ENDPOINT) @@ -290,28 +297,16 @@ 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) + Map sendInfluxMetricsRequest() { + def response = given(influxRequestSpecification) + .queryParams(["db": influxdbContainer.getDatabase(), + "q" : "SELECT COUNT(count) FROM /.*/ WHERE count >= 1 GROUP BY \"measurement\""]) + .get(INFLUX_DB_ENDPOINT) checkResponseStatusCode(response) - decode(response.body.asString(), LineItemStatusReport) + collectInToMap(decode(response.getBody().asString(), InfluxResponse)) } - @Step("[GET] /metrics") String sendPrometheusMetricsRequest() { def response = given(prometheusRequestSpecification).get(PROMETHEUS_METRICS_ENDPOINT) @@ -328,8 +323,8 @@ class PrebidServerService implements ObjectMapperWrapper { def payload = encode(bidRequest) given(requestSpecification).headers(headers) - .body(payload) - .post(AUCTION_ENDPOINT) + .body(payload) + .post(AUCTION_ENDPOINT) } private Response postCookieSync(CookieSyncRequest cookieSyncRequest, @@ -363,10 +358,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) { @@ -378,16 +381,32 @@ class PrebidServerService implements ObjectMapperWrapper { } } - private static Map getHeaders(Response response) { - response.headers().collectEntries { [it.name, it.value] } + private static Map> getHeaders(Response response) { + response.headers().groupBy { it.name }.collectEntries { [(it.key): it.value*.value] } } private static UidsCookie getDecodedUidsCookie(Response response) { - def uids = response.detailedCookie(UIDS_COOKIE_NAME)?.value - if (uids) { - return decode(new String(Base64.urlDecoder.decode(uids)), UidsCookie) - } else { - throw new IllegalStateException("uids cookie is missing in response") + def sortedCookies = response.detailedCookies() + .findAll { cookie -> !(cookie =~ /\buids\d*=\s*;/) } + .sort { a, b -> + def aMatch = (a.name =~ /uids(\d*)/)[0] + def bMatch = (b.name =~ /uids(\d*)/)[0] + + def aNumber = (aMatch?.getAt(1) ? aMatch[1].toInteger() : 0) + def bNumber = (bMatch?.getAt(1) ? bMatch[1].toInteger() : 0) + + aNumber <=> bNumber + } + + def decodedCookiesList = sortedCookies.collect { cookie -> + def uid = (cookie =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + decodedCookiesList.inject(new UidsCookie()) { uidsCookie, decodedCookie -> + uidsCookie.uids = (uidsCookie.uids ?: new LinkedHashMap()) + (decodedCookie.uids ?: new LinkedHashMap()) + uidsCookie.tempUIDs = (uidsCookie.tempUIDs ?: new LinkedHashMap()) + (decodedCookie.tempUIDs ?: new LinkedHashMap()) + uidsCookie } } @@ -395,8 +414,8 @@ 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") - .withZone(ZoneId.from(UTC)) + 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 +432,35 @@ 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) { + try { + PBSUtils.waitUntil({ getPbsLogsByValue(value) != null }) + true + } catch (IllegalStateException ignored) { + false + } + } + + Boolean isContainMetricByValue(String value) { + try { + PBSUtils.waitUntil({ sendInfluxMetricsRequest()[value] != null }) + true + } catch (IllegalStateException ignored) { + false + } + } + + 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) @@ -429,7 +477,13 @@ class PrebidServerService implements ObjectMapperWrapper { private static RequestSpecification buildAndGetRequestSpecification(String uri, AuthenticationScheme authScheme) { new RequestSpecBuilder().setBaseUri(uri) - .setAuth(authScheme) - .build() + .setAuth(authScheme) + .build() + } + + private static Map collectInToMap(InfluxResponse responseBody) { + responseBody?.results?.first()?.series?.collectEntries { + [(it.name): it.values?.first()?.getAt(1) as Integer] + } ?: [:] } } 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..ab614e0ca5f 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/Dependencies.groovy @@ -2,11 +2,16 @@ package org.prebid.server.functional.testcontainers import org.prebid.server.functional.testcontainers.container.NetworkServiceContainer import org.prebid.server.functional.util.SystemProperties +import org.testcontainers.containers.InfluxDBContainer 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 +25,41 @@ 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 InfluxDBContainer influxdbContainer = new InfluxDBContainer<>(DockerImageName.parse("influxdb:1.8.10")) + .withUsername("prebid") + .withPassword("prebid") + .withAuthEnabled(false) + .withDatabase("prebid") .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, influxdbContainer]).join() } } static void stop() { if (IS_LAUNCH_CONTAINERS) { - [networkServiceContainer, mysqlContainer].parallelStream() - .forEach({ it.stop() }) + [networkServiceContainer, mysqlContainer, localStackContainer, influxdbContainer].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..02d627f141a 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -1,10 +1,13 @@ package org.prebid.server.functional.testcontainers +import org.testcontainers.containers.InfluxDBContainer 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 import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY final class PbsConfig { @@ -28,7 +31,7 @@ LIMIT 1 static final Map DEFAULT_ENV = [ "logging.sampling-rate" : "1.0", - "auction.ad-server-currency" : "USD", + "auction.ad-server-currency" : DEFAULT_CURRENCY.value, "auction.stored-requests-timeout-ms" : "1000", "metrics.prefix" : "prebid", "status-response" : "ok", @@ -88,14 +91,25 @@ LIMIT 1 } static Map getMySqlConfig(MySQLContainer mysql = Dependencies.mysqlContainer) { - ["settings.database.type" : "mysql", - "settings.database.host" : mysql.getNetworkAliases().get(0), - "settings.database.port" : mysql.exposedPorts.get(0) as String, - "settings.database.dbname" : mysql.databaseName, - "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.type" : "mysql", + "settings.database.host" : mysql.getNetworkAliases().get(0), + "settings.database.port" : mysql.exposedPorts.get(0) as String, + "settings.database.dbname" : mysql.databaseName, + "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.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 +119,34 @@ 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.cwire.meta-info.app-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" : ""] + } + + static Map getCurrencyConverterConfig() { + ["auction.ad-server-currency" : DEFAULT_CURRENCY.value, + "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"] + } + + static Map getTargetingConfig() { + ["settings.targeting.truncate-attr-chars": '255'] } 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..8022f2e8dcc 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,9 @@ class NetworkServiceContainer extends MockServerContainer { NetworkServiceContainer(String version) { super(DockerImageName.parse("mockserver/mockserver:mockserver-$version")) - withCommand("-serverPort $PORT -logLevel WARN") + def aliasWithTopLevelDomain = "${getNetworkAliases().first()}.com".toString() + withCreateContainerCmdModifier { it.withHostName(aliasWithTopLevelDomain) } + setNetworkAliases([aliasWithTopLevelDomain]) } String getHostAndPort() { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy index 5629826942b..70509252af6 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy @@ -5,6 +5,7 @@ import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.util.SystemProperties import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.images.builder.Transferable import static org.prebid.server.functional.testcontainers.PbsConfig.DEFAULT_ENV @@ -41,6 +42,7 @@ class PrebidServerContainer extends GenericContainer { << PbsConfig.bidderAliasConfig << PbsConfig.prebidCacheConfig << PbsConfig.mySqlConfig + << PbsConfig.targetingConfig withConfig(commonConfig) withConfig(customConfig) } @@ -73,6 +75,10 @@ class PrebidServerContainer extends GenericContainer { getMappedPort(PROMETHEUS_PORT) } + String getInfluxUri() { + return "http://$host:$Dependencies.influxdbContainer.firstMappedPort" + } + String getRootUri() { return "http://$host:$port" } @@ -91,11 +97,18 @@ class PrebidServerContainer extends GenericContainer { private static String normalizeProperty(String property) { property.replace(".", "_") - .replace("-", "") .replace("[", "_") .replace("]", "_") } + PrebidServerContainer withFolder(String containerPath) { + this.withCopyToContainer( + Transferable.of(new byte[0], 010755), + containerPath + "/.keep" + ) + return this + } + // This is a workaround for cases when container is killed mid-test due to OOM void refresh() { if (!running) { 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..05d6fcfa3d7 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 @@ -19,25 +19,23 @@ import static org.mockserver.model.JsonPathBody.jsonPath class Bidder extends NetworkScaffolding { - private static final String AUCTION_ENDPOINT = "/auction" - - Bidder(MockServerContainer mockServerContainer) { - super(mockServerContainer, AUCTION_ENDPOINT) + Bidder(MockServerContainer mockServerContainer, String endpoint = "/auction") { + super(mockServerContainer, endpoint) } @Override protected HttpRequest getRequest(String bidRequestId) { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) .withBody(jsonPath("\$[?(@.id == '$bidRequestId')]")) } @Override protected HttpRequest getRequest() { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) } HttpRequest getRequest(String bidRequestId, String requestMatchPath) { - request().withPath(AUCTION_ENDPOINT) + request().withPath(endpoint) .withBody(jsonPath("\$[?(@.$requestMatchPath == '$bidRequestId')]")) } @@ -77,7 +75,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(width: 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/CurrencyConversion.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy index 6f5b74bda61..6246f8c9f4d 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy @@ -7,16 +7,18 @@ 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.util.CurrencyUtil.DEFAULT_CURRENCY_RATES class CurrencyConversion extends NetworkScaffolding { static final String CURRENCY_ENDPOINT_PATH = "/currency" + private static final CurrencyConversionRatesResponse DEFAULT_RATES_RESPONSE = CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES) CurrencyConversion(MockServerContainer mockServerContainer) { super(mockServerContainer, CURRENCY_ENDPOINT_PATH) } - void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse) { + void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse = DEFAULT_RATES_RESPONSE) { setResponse(request, conversionRatesResponse) } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy index de271b4123a..5af648b2bc0 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/HttpSettings.groovy @@ -1,13 +1,21 @@ package org.prebid.server.functional.testcontainers.scaffolding +import org.mockserver.matchers.Times +import org.mockserver.model.Header import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.ResponseModel 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.MediaType.APPLICATION_JSON class HttpSettings extends NetworkScaffolding { private static final String ENDPOINT = "/stored-requests" + private static final String RFC_ENDPOINT = "/stored-requests-rfc" private static final String AMP_ENDPOINT = "/amp-stored-requests" HttpSettings(MockServerContainer mockServerContainer) { @@ -27,12 +35,47 @@ class HttpSettings extends NetworkScaffolding { @Override void setResponse() { + } + + protected HttpRequest getRfcRequest(String accountId) { + request().withPath(RFC_ENDPOINT) + .withQueryStringParameter("account-id", accountId) + } + + + void setRfcResponse(String value, + ResponseModel responseModel, + HttpStatusCode statusCode = OK_200, + Map headers = [:]) { + def responseHeaders = headers.collect { new Header(it.key, it.value) } + def mockResponse = encode(responseModel) + mockServerClient.when(getRfcRequest(value), Times.unlimited()) + .respond(response().withStatusCode(statusCode.code()) + .withBody(mockResponse, APPLICATION_JSON) + .withHeaders(responseHeaders)) + } + int getRfcRequestCount(String value) { + mockServerClient.retrieveRecordedRequests(getRfcRequest(value)) + .size() } @Override void reset() { super.reset(ENDPOINT) + super.reset(RFC_ENDPOINT) super.reset(AMP_ENDPOINT) } + + static String getEndpoint() { + return ENDPOINT + } + + static String getAmpEndpoint() { + return AMP_ENDPOINT + } + + static String getRfcEndpoint() { + return RFC_ENDPOINT + } } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy index 7eea7536dfb..8ac5ad41483 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/NetworkScaffolding.groovy @@ -37,17 +37,17 @@ abstract class NetworkScaffolding implements ObjectMapperWrapper { int getRequestCount(HttpRequest httpRequest) { mockServerClient.retrieveRecordedRequests(httpRequest) - .size() + .size() } int getRequestCount(String value) { mockServerClient.retrieveRecordedRequests(getRequest(value)) - .size() + .size() } int getRequestCount() { mockServerClient.retrieveRecordedRequests(request) - .size() + .size() } void setResponse(HttpRequest httpRequest, @@ -56,8 +56,8 @@ abstract class NetworkScaffolding implements ObjectMapperWrapper { Times times = Times.exactly(1)) { def mockResponse = encode(responseModel) mockServerClient.when(httpRequest, times) - .respond(response().withStatusCode(statusCode.code()) - .withBody(mockResponse, APPLICATION_JSON)) + .respond(response().withStatusCode(statusCode.code()) + .withBody(mockResponse, APPLICATION_JSON)) } void setResponse(String value, @@ -73,9 +73,9 @@ abstract class NetworkScaffolding implements ObjectMapperWrapper { def responseHeaders = headers.collect { new Header(it.key, it.value) } def mockResponse = encode(responseModel) mockServerClient.when(getRequest(value), Times.unlimited()) - .respond(response().withStatusCode(statusCode.code()) - .withBody(mockResponse, APPLICATION_JSON) - .withHeaders(responseHeaders)) + .respond(response().withStatusCode(statusCode.code()) + .withBody(mockResponse, APPLICATION_JSON) + .withHeaders(responseHeaders)) } void setResponse(String value, @@ -86,39 +86,39 @@ abstract class NetworkScaffolding implements ObjectMapperWrapper { def responseHeaders = headers.collect { new Header(it.key, it.value) } def mockResponse = encode(responseModel) mockServerClient.when(getRequest(value), Times.unlimited()) - .respond(response().withStatusCode(statusCode.code()) - .withBody(mockResponse, APPLICATION_JSON) - .withHeaders(responseHeaders) - .withDelay(TimeUnit.MILLISECONDS, responseDelay)) + .respond(response().withStatusCode(statusCode.code()) + .withBody(mockResponse, APPLICATION_JSON) + .withHeaders(responseHeaders) + .withDelay(TimeUnit.MILLISECONDS, responseDelay)) } void setResponse(String value, String mockResponse) { mockServerClient.when(getRequest(value), Times.exactly(1)) - .respond(response().withStatusCode(OK_200.code()) - .withBody(mockResponse, APPLICATION_JSON)) + .respond(response().withStatusCode(OK_200.code()) + .withBody(mockResponse, APPLICATION_JSON)) } void setResponse(ResponseModel responseModel) { def mockResponse = encode(responseModel) mockServerClient.when(request().withPath(endpoint)) - .respond(response().withStatusCode(OK_200.code()) - .withBody(mockResponse, APPLICATION_JSON)) + .respond(response().withStatusCode(OK_200.code()) + .withBody(mockResponse, APPLICATION_JSON)) } void setResponse(String value, HttpStatusCode httpStatusCode) { mockServerClient.when(getRequest(value), Times.exactly(1)) - .respond(response().withStatusCode(httpStatusCode.code())) + .respond(response().withStatusCode(httpStatusCode.code())) } void setResponse(String value, HttpStatusCode httpStatusCode, String errorText) { mockServerClient.when(getRequest(value), Times.exactly(1)) - .respond(response().withStatusCode(httpStatusCode.code()) - .withBody(errorText, APPLICATION_JSON)) + .respond(response().withStatusCode(httpStatusCode.code()) + .withBody(errorText, APPLICATION_JSON)) } void setResponseWithTimeout(String value, int timeoutSec = 5) { mockServerClient.when(getRequest(value), Times.exactly(1)) - .respond(response().withDelay(SECONDS, timeoutSec)) + .respond(response().withDelay(SECONDS, timeoutSec)) } protected def getRequestAndResponse() { @@ -130,14 +130,19 @@ abstract class NetworkScaffolding implements ObjectMapperWrapper { .collect { it.body.toString() } } + String getRecordedRequestsQueryParameters(HttpRequest httpRequest) { + mockServerClient.retrieveRecordedRequests(httpRequest) + .collect { it -> it.queryStringParameters.multimap.toString()} + } + List getRecordedRequestsBody(String value) { mockServerClient.retrieveRecordedRequests(getRequest(value)) - .collect { it.body.toString() } + .collect { it.body.toString() } } List getRecordedRequestsBody() { mockServerClient.retrieveRecordedRequests(request) - .collect { it.body.toString() } + .collect { it.body.toString() } } Map> getLastRecordedRequestHeaders(HttpRequest httpRequest) { 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..66ce54a9531 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,13 +6,16 @@ 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.prebid.server.functional.model.response.vtrack.TransferValue +import org.prebid.server.functional.util.PBSUtils import org.testcontainers.containers.MockServerContainer -import java.util.stream.Collectors import java.util.stream.Stream import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500 import static org.mockserver.model.HttpStatusCode.OK_200 import static org.mockserver.model.JsonPathBody.jsonPath @@ -24,6 +27,11 @@ class PrebidCache extends NetworkScaffolding { super(mockServerContainer, CACHE_ENDPOINT) } + String getVTracGetRequestParams() { + getRecordedRequestsQueryParameters(request().withMethod("GET") + .withPath(CACHE_ENDPOINT)) + } + void setXmlCacheResponse(String payload, PrebidCacheResponse prebidCacheResponse) { setResponse(getXmlCacheRequest(payload), prebidCacheResponse) } @@ -43,28 +51,68 @@ class PrebidCache extends NetworkScaffolding { @Override protected HttpRequest getRequest(String impId) { request().withMethod("POST") - .withPath(CACHE_ENDPOINT) - .withBody(jsonPath("\$.puts[?(@.value.impid == '$impId')]")) + .withPath(CACHE_ENDPOINT) + .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") - .withPath(CACHE_ENDPOINT) + .withPath(CACHE_ENDPOINT) } @Override void setResponse() { - mockServerClient.when(request().withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) - .respond{request -> request.withPath(endpoint) - ? response().withStatusCode(OK_200.code()).withBody(getBodyByRequest(request)) - : HttpResponse.notFoundResponse()} + mockServerClient.when(request() + .withMethod("POST") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { request -> + request.withPath(endpoint) + ? response().withStatusCode(OK_200.code()).withBody(getBodyByRequest(request)) + : HttpResponse.notFoundResponse() + } + } + + void setGetResponse(TransferValue vTrackResponse) { + mockServerClient.when(request() + .withMethod("GET") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { request -> + request.withPath(endpoint) + ? response().withStatusCode(OK_200.code()).withBody(encode(vTrackResponse)) + : HttpResponse.notFoundResponse() + } + } + + void setInvalidPostResponse() { + mockServerClient.when(request() + .withMethod("POST") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(INTERNAL_SERVER_ERROR_500.code()) } + } + + void setInvalidGetResponse(String uuid, String errorMessage = PBSUtils.randomString) { + mockServerClient.when(request() + .withMethod("GET") + .withPath(endpoint) + .withQueryStringParameter("uuid", uuid), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withBody(errorMessage).withStatusCode(INTERNAL_SERVER_ERROR_500.code()) } + } private static HttpRequest getXmlCacheRequest(String payload) { request().withMethod("POST") - .withPath(CACHE_ENDPOINT) - .withBody(jsonPath("\$.puts[?(@.value =~/^.*$payload.*\$/)]")) + .withPath(CACHE_ENDPOINT) + .withBody(jsonPath("\$.puts[?(@.value =~/^.*$payload.*\$/)]")) } private String getBodyByRequest(HttpRequest request) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy new file mode 100644 index 00000000000..c8ad7caa924 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/StoredCache.groovy @@ -0,0 +1,119 @@ +package org.prebid.server.functional.testcontainers.scaffolding + +import org.mockserver.matchers.TimeToLive +import org.mockserver.matchers.Times +import org.mockserver.model.HttpRequest +import org.mockserver.model.HttpStatusCode +import org.prebid.server.functional.model.config.Audience +import org.prebid.server.functional.model.config.AudienceId +import org.prebid.server.functional.model.config.IdentifierType +import org.prebid.server.functional.model.config.OptableTargetingConfig +import org.prebid.server.functional.model.config.TargetingOrtb +import org.prebid.server.functional.model.config.TargetingResult +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.util.PBSUtils +import org.testcontainers.containers.MockServerContainer + +import java.nio.charset.StandardCharsets + +import static org.mockserver.model.HttpRequest.request +import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.HttpStatusCode.NO_CONTENT_204 +import static org.mockserver.model.HttpStatusCode.OK_200 + +class StoredCache extends NetworkScaffolding { + + private static final String CACHE_ENDPOINT = "/stored-cache" + + StoredCache(MockServerContainer mockServerContainer) { + super(mockServerContainer, CACHE_ENDPOINT) + } + + @Override + protected HttpRequest getRequest(String impId) {} + + @Override + HttpRequest getRequest() { + request().withMethod("GET") + .withPath(endpoint) + } + + @Override + void setResponse() {} + + TargetingResult setTargetingResponse(BidRequest bidRequest, OptableTargetingConfig config) { + def targetingResult = getBodyByRequest(bidRequest) + mockServerClient.when(request() + .withMethod("GET") + .withPath("$endpoint${QueryBuilder.buildQuery(bidRequest, config)}"), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(OK_200.code()).withBody(encode(targetingResult)) } + targetingResult + } + + TargetingResult setCachedTargetingResponse(BidRequest bidRequest) { + def targetingResult = getBodyByRequest(bidRequest) + mockServerClient.when(request() + .withMethod("GET") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(OK_200.code()).withBody(encode(targetingResult)) } + targetingResult + } + + void setCachingResponse(HttpStatusCode statusCode = NO_CONTENT_204) { + mockServerClient.when(request() + .withMethod("POST") + .withPath(endpoint), Times.unlimited(), TimeToLive.unlimited(), -10) + .respond { response().withStatusCode(statusCode.code()) } + } + + private static TargetingResult getBodyByRequest(BidRequest bidRequest) { + new TargetingResult().tap { + it.audience = [new Audience(ids: [new AudienceId(id: PBSUtils.randomString)], provider: PBSUtils.randomString)] + it.ortb2 = new TargetingOrtb(user: new User(data: bidRequest.user.data, eids: bidRequest.user.eids)) + } + } + + private class QueryBuilder { + + static String buildQuery(BidRequest bidRequest, OptableTargetingConfig config) { + buildIdsString(config) + buildAttributesString(bidRequest, config) + } + + private static String buildIdsString(OptableTargetingConfig config) { + def ppids = config.ppidMapping + if (!ppids) { + return '' + } + + def reorderedIds = reorderIds(ppids.keySet(), config.idPrefixOrder) + + reorderedIds.collect { id -> + def value = ppids[id] + "&id=${URLEncoder.encode("${id.value}:${value}", StandardCharsets.UTF_8)}" + }.join('') + } + + private static Set reorderIds(Set ids, String idPrefixOrder) { + if (!idPrefixOrder) { + return ids + } + def prefixOrder = idPrefixOrder.split(',') as List + def prefixToPriority = prefixOrder.collectEntries { v, i -> [(v): i] } + ids.sort { prefixToPriority.get(it.value, Integer.MAX_VALUE) } + } + + private static String buildAttributesString(BidRequest bidRequest, OptableTargetingConfig config) { + def regs = bidRequest.regs + def gdpr = regs?.gdpr + def gdprConsent = bidRequest.user?.consent + + [gdprConsent != null ? "&gdpr_consent=${gdprConsent}" : null, + "&gdpr=${gdpr ? 1 : 0}", + regs?.gpp ? "&gpp=${regs.gpp}" : null, + regs?.gppSid ? "&gpp_sid=${regs.gppSid.first()}" : null, + config?.timeout ? "&timeout=${config.timeout}ms" : null, + "&osdk=prebid-server"].findAll().join('') + } + } +} 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..9013978f76f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy @@ -1,20 +1,41 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.bidder.AppNexus 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.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST 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.BOGUS import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.GENER_X +import static org.prebid.server.functional.model.bidder.BidderName.OPENX import static org.prebid.server.functional.model.bidder.CompressionType.GZIP -import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER class AliasSpec extends BaseSpec { + private static final Map ADDITIONAL_BIDDERS_CONFIG = ["adapters.${OPENX.value}.enabled" : "true", + "adapters.${OPENX.value}.endpoint" : "$networkServiceContainer.rootUri/${OPENX.value}/auction".toString(), + "adapters.${APPNEXUS.value}.enabled" : "true", + "adapters.${APPNEXUS.value}.endpoint": "$networkServiceContainer.rootUri/${APPNEXUS.value}/auction".toString()] + private static PrebidServerService pbsServiceWithAdditionalBidders + + def setupSpec() { + pbsServiceWithAdditionalBidders = pbsServiceFactory.getService(ADDITIONAL_BIDDERS_CONFIG + GENERIC_ALIAS_CONFIG) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(ADDITIONAL_BIDDERS_CONFIG + GENERIC_ALIAS_CONFIG) + } + def "PBS should be able to take alias from request"() { given: "Default bid request with alias" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -89,7 +110,7 @@ class AliasSpec extends BaseSpec { then: "Request should fail with error" def exception = thrown(PrebidServerException) assert exception.responseBody.contains("Invalid request format: request.ext.prebid.aliasgvlids. " + - "vendorId ${validId} refers to unknown bidder alias: ${bidderName}") + "vendorId ${validId} refers to unknown bidder alias: ${bidderName.toLowerCase()}") } def "PBS should return an error when GVL ID alias value is lower that one"() { @@ -105,7 +126,7 @@ class AliasSpec extends BaseSpec { then: "Request should fail with error" def exception = thrown(PrebidServerException) assert exception.responseBody.contains("Invalid request format: request.ext.prebid.aliasgvlids. " + - "Invalid vendorId ${invalidId} for alias: ${bidderName}. Choose a different vendorId, or remove this entry.") + "Invalid vendorId ${invalidId} for alias: ${bidderName.toLowerCase()}. Choose a different vendorId, or remove this entry.") where: invalidId << [PBSUtils.randomNegativeNumber, 0] @@ -124,26 +145,132 @@ class AliasSpec extends BaseSpec { then: "Request should fail with an error" def exception = thrown(PrebidServerException) assert exception.statusCode == BAD_REQUEST.code() - assert exception.responseBody == "Invalid request format: request.ext.prebid.aliases.$randomString " + + assert exception.responseBody == "Invalid request format: request.ext.prebid.aliases.${randomString.toLowerCase()} " + "refers to unknown bidder: $BOGUS.value" } 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()]) - - and: "Default bid request with alias" + given: "Default bid request with alias" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].ext.prebid.bidder.alias = new Generic() } when: "PBS processes auction request" - prebidServerService.sendAuctionRequest(bidRequest) + pbsServiceWithAdditionalBidders.sendAuctionRequest(bidRequest) then: "Bidder request should contain request per-alies" def bidderRequests = bidder.getBidderRequests(bidRequest.id) assert bidderRequests.size() == 2 } + + def "PBS should ignore aliases for requests with a base adapter"() { + given: "PBs server with aliases config" + def pbsConfig = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/openx/auction".toString()] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with openx and alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + imp[0].ext.prebid.bidder.generic = new Generic() + ext.prebid.aliases = [(OPENX.value): GENERIC] + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS contain two http calls and the different url for both" + def responseDebug = bidResponse.ext.debug + assert responseDebug.httpcalls.size() == 2 + assert responseDebug.httpcalls[OPENX.value]*.uri == ["$networkServiceContainer.rootUri/openx/auction"] + assert responseDebug.httpcalls[GENERIC.value]*.uri == ["$networkServiceContainer.rootUri/auction"] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should validate request as alias request and without any warnings when required properties in place"() { + given: "Default bid request with openx and alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.appNexus = AppNexus.default + imp[0].ext.prebid.bidder.generic = null + ext.prebid.aliases = [(APPNEXUS.value): OPENX] + } + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithAdditionalBidders.sendAuctionRequest(bidRequest) + + then: "PBS contain http call for specific bidder" + def responseDebug = bidResponse.ext.debug + assert responseDebug.httpcalls.size() == 1 + assert responseDebug.httpcalls[APPNEXUS.value]*.uri == ["$networkServiceContainer.rootUri/${APPNEXUS.value}/auction"] + + and: "PBS should not contain any warnings" + assert !bidResponse.ext.warnings + } + + def "PBS should validate request as alias request and emit proper warnings when validation fails for request"() { + given: "Default bid request with openx and alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.appNexus = new AppNexus() + imp[0].ext.prebid.bidder.generic = null + ext.prebid.aliases = [(APPNEXUS.value): OPENX] + } + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithAdditionalBidders.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't contain http calls" + assert !bidResponse.ext.debug.httpcalls + + and: "Bid response should contain warning" + assert bidResponse.ext?.warnings[PREBID]?.code == [999, 999] + assert bidResponse.ext?.warnings[PREBID]*.message == + ["WARNING: request.imp[0].ext.prebid.bidder.${APPNEXUS.value} was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder.${APPNEXUS.value} failed validation.\n" + + "\$: must be valid to one and only one schema, but 0 are valid\n" + + "\$: required property 'placement_id' not found\n" + + "\$: required property 'inv_code' not found\n" + + "\$: required property 'placementId' not found\n" + + "\$: required property 'member' not found\n" + + "\$: required property 'invCode' not found", + "WARNING: request.imp[0].ext must contain at least one valid bidder"] + } + + def "PBS should invoke as aliases when alias is unknown and core bidder is specified"() { + given: "Default bid request with generic and alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.generX = new Generic() + ext.prebid.aliases = [(GENER_X.value): GENERIC] + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS contain two http calls and the same url for both" + def responseDebug = bidResponse.ext.debug + assert responseDebug.httpcalls.size() == 2 + assert responseDebug.httpcalls[GENER_X.value]*.uri == responseDebug.httpcalls[GENERIC.value]*.uri + + and: "Bidder request should contain request per-alies" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert bidderRequests.size() == 2 + } + + def "PBS should invoke aliases when alias is unknown and no core bidder is specified"() { + given: "Default bid request with generic and alias bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.generX = new Generic() + imp[0].ext.prebid.bidder.generic = null + ext.prebid.aliases = [(GENER_X.value): GENERIC] + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS contain two http calls and the same url for both" + def responseDebug = bidResponse.ext.debug + assert responseDebug.httpcalls.size() == 1 + assert responseDebug.httpcalls[GENER_X.value] + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy new file mode 100644 index 00000000000..1e2c67fad6f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/AlternateBidderCodeSpec.groovy @@ -0,0 +1,1833 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AlternateBidderCodes +import org.prebid.server.functional.model.config.BidderConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.request.auction.Amx +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.StoredBidResponse +import org.prebid.server.functional.model.request.auction.Targeting +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.ErrorType +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Shared + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +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.AMX +import static org.prebid.server.functional.model.bidder.BidderName.AMX_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.BOGUS +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.UNKNOWN +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.RESPONSE_REJECTED_GENERAL +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class AlternateBidderCodeSpec extends BaseSpec { + + private static final String ADAPTER_RESPONSE_VALIDATION_METRICS = "adapter.%s.response.validation.seat" + private static final String ERROR_BID_CODE_VALIDATION = "BidId `%s` validation messages: " + + "Error: invalid bidder code %s was set by the adapter %s for the account %s" + private static final String INVALID_BIDDER_CODE_LOGS = "invalid bidder code %s was set by the adapter %s for the account %s" + private static final Map AMX_CONFIG = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + @Shared + private static final PrebidServerService pbsServiceWithAmxBidder = pbsServiceFactory.getService(AMX_CONFIG) + + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(AMX_CONFIG) + } + + def "PBS shouldn't discard bid amx alias when soft alias request with allowed bidder code"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [ALIAS] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${ALIAS}"] + assert targeting["hb_size_${ALIAS}"] + assert targeting["hb_bidder"] == ALIAS.value + assert targeting["hb_bidder_${ALIAS}"] == ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(ALIAS)] + } + + def "PBS should populate meta demand source when bid response with demand source"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Bid response with demand source" + def demandSource = PBSUtils.getRandomString() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(demandSource: demandSource) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [AMX] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain demand source" + assert response.seatbid.bid.ext.prebid.meta.demandSource.flatten() == [demandSource] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + + def "PBS shouldn't populate meta demand source when bid response without demand source"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Bid response without demand source" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(demandSource: null) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [AMX] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + + def "PBS shouldn't discard bid for amx bidder same seat in response as seat in bid.ext.bidderCode"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: bidderCode) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [bidderCode] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${bidderCode}"] + assert targeting["hb_size_${bidderCode}"] + assert targeting["hb_bidder"] == bidderCode.value + assert targeting["hb_bidder_${bidderCode}"] == bidderCode.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(bidderCode.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + bidderCode << [AMX, AMX_CAMEL_CASE] + } + + def "PBS should discard bid for amx bidder when imp[].bidder isn't same as in bid.ext.bidderCode"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: bidderName) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == bidderName + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, bidderName, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(bidderName, AMX, bidRequest.accountId)) + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should emit metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + bidderName << [BOGUS, UNKNOWN, WILDCARD] + } + + def "PBS should discard bid amx alias requested when imp[].bidder isn't same as in bid.ext.bidderCode"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: bidderName) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == bidderName + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.ALIAS][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, bidderName, ALIAS, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(bidderName, ALIAS, bidRequest.accountId)) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "PBS should emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(ALIAS)] + + where: + bidderName << [BOGUS, UNKNOWN, WILDCARD] + } + + def "PBS shouldn't discard bid amx alias requested when imp[].bidder is same as in bid.ext.bidderCode and alternate bidder code allow"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + ext.prebid.alternateBidderCodes = requestAlternateBidderCode + } + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes = accountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.seat == [GENERIC] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(ALIAS)] + + where: + requestAlternateBidderCode | accountAlternateBidderCodes + new AlternateBidderCodes(enabled: true, bidders: [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) | null + null | new AlternateBidderCodes(enabled: true, bidders: [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) + } + + def "PBS shouldn't discard bid amx alias requested when imp[].bidder is same as in bid.ext.bidderCode"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: bidderCode) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.seat == [bidderCode] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${bidderCode}"] + assert targeting["hb_size_${bidderCode}"] + assert targeting["hb_bidder"] == bidderCode.value + assert targeting["hb_bidder_${bidderCode}"] == bidderCode.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(bidderCode.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(bidderCode)] + + where: + bidderCode << [ALIAS, ALIAS_CAMEL_CASE] + } + + def "PBS shouldn't discard the bid or emit a response warning when account alternate bidder codes not fully configured"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes = accountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings,errors and seatnonbid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + accountAlternateBidderCodes << [null, + new AlternateBidderCodes(), + new AlternateBidderCodes(enabled: true), + new AlternateBidderCodes(enabled: false), + new AlternateBidderCodes(bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(enabled: true, bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(enabled: false, bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(enabled: false, bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]), + new AlternateBidderCodes(bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])]), + new AlternateBidderCodes(enabled: false, bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]), + new AlternateBidderCodes(enabled: false, bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])]), + new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]), + new AlternateBidderCodes(enabled: true, bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])])] + } + + def "PBS shouldn't discard the bid or emit a response warning when request alternate bidder codes not fully configured"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes = requestedAlternateBidderCodes + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings,errors and seatnonbid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + requestedAlternateBidderCodes << [null, + new AlternateBidderCodes(), + new AlternateBidderCodes(enabled: true), + new AlternateBidderCodes(enabled: false), + new AlternateBidderCodes(bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(enabled: false, bidders: [(AMX): new BidderConfig()]), + new AlternateBidderCodes(bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]), + new AlternateBidderCodes(enabled: false, bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]), + new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: false, allowedBidderCodes: [UNKNOWN])]),] + } + + def "PBS should validate and throw error when request alternate bidder codes not fully configured"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes = requestedAlternateBidderCodes + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == "Invalid request format: " + + "request.ext.prebid.alternatebiddercodes.bidders.unknown is not a known bidder or alias" + + + where: + requestedAlternateBidderCodes << [new AlternateBidderCodes(bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(enabled: true, bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(enabled: false, bidders: [(UNKNOWN): new BidderConfig()]), + new AlternateBidderCodes(bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])]), + new AlternateBidderCodes(enabled: false, bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])]), + new AlternateBidderCodes(enabled: true, bidders: [(UNKNOWN): new BidderConfig(enabled: false, allowedBidderCodes: [AMX])])] + } + + def "PBS shouldn't discard bid when alternate bidder code allows bidder codes fully configured and bidder requested in uppercase"() { + given: "Default bid request with AMX bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.tap { + amxUpperCase = new Amx() + amx = null + } + ext.prebid.alternateBidderCodes.bidders[AMX].allowedBidderCodesLowerCase = [GENERIC] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == GENERIC + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response shouldn't contain warnings,errors and seatnonbid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + } + + def "PBS shouldn't discard bid when alternate bidder code allows bidder codes fully configured with different case"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config = configAccountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == GENERIC + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings,errors and seatnonbid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + configAccountAlternateBidderCodes << [ + new AccountConfig(alternateBidderCodesSnakeCase: new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodesSnakeCase: [GENERIC])])), + new AccountConfig(alternateBidderCodes: new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodesSnakeCase: [GENERIC])])), + new AccountConfig(alternateBidderCodesSnakeCase: new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]))] + } + + def "PBS should take precede of request and discard the bid and emit a response error when alternate bidder codes enabled and bidder came with different bidder code"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes.bidders[AMX].allowedBidderCodes = [UNKNOWN] + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: UNKNOWN) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == UNKNOWN + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, UNKNOWN, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(UNKNOWN, AMX, bidRequest.accountId)) + + and: "PBS should emit metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + } + + def "PBS should discard the bid and emit a response warning when alternate bidder codes disabled and bidder came with different bidderCode"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes.enabled = requestedAlternateBidderCodes + } + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes.enabled = accountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: UNKNOWN) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == UNKNOWN + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, UNKNOWN, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(UNKNOWN, AMX, bidRequest.accountId)) + + and: "PBS should emit metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + where: + requestedAlternateBidderCodes | accountAlternateBidderCodes + false | true + false | false + false | null + null | false + } + + def "PBS shouldn't discard the bid or emit a response warning when account alternate bidder codes are enabled and allowed bidder codes are either a wildcard or empty"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes.bidders[AMX].allowedBidderCodes = accountAllowedBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid.seat.flatten() == [GENERIC] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response shouldn't contain warnings and errors and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBs metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + accountAllowedBidderCodes << [[WILDCARD], [WILDCARD, EMPTY], [EMPTY, WILDCARD], null] + } + + def "PBS shouldn't discard the bid or emit a response warning when request alternate bidder codes are enabled and allowed bidder codes are either a wildcard or empty"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes.bidders[AMX].allowedBidderCodesLowerCase = requestedAllowedBidderCodes + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid.seat.flatten() == [GENERIC] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and errors and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBs metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + requestedAllowedBidderCodes << [[WILDCARD], [WILDCARD, EMPTY], [EMPTY, WILDCARD], null] + } + + def "PBS shouldn't discard the bid or emit a response warning when request alternate bidder codes are enabled and the allowed bidder codes is same as bidder's request"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes.bidders = requestAlternateBidders + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid.seat.flatten() == [GENERIC] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and errors and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBs metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + requestAlternateBidders << [[(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])], + [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC_CAMEL_CASE])], + [(AMX_CAMEL_CASE): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC_CAMEL_CASE])], + [(AMX_CAMEL_CASE): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]] + } + + def "PBS shouldn't discard the bid or emit a response warning when account alternate bidder codes are enabled and the allowed bidder codes is same as bidder's request"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes.bidders = accountAlternateBidders + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid.seat.flatten() == [GENERIC] + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Response shouldn't contain warnings and errors and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBs metric shouldn't be updated" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + accountAlternateBidders << [[(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])], + [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC_CAMEL_CASE])], + [(AMX_CAMEL_CASE): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC_CAMEL_CASE])], + [(AMX_CAMEL_CASE): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]] + } + + def "PBS shouldn't discard the bid or emit a response warning when default account alternate bidder codes are enabled and the allowed bidder codes match the bidder's request"() { + given: "Pbs config with default-account-config of alternate bidder code" + def defaultAccountConfig = AccountConfig.defaultAccountConfig.tap { + alternateBidderCodes = new AlternateBidderCodes().tap { + it.enabled = true + it.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + def config = AMX_CONFIG + ["settings.default-account-config": encode(defaultAccountConfig)] + def pbsService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + ext.prebid.alternateBidderCodes = null + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsService) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain seatbid.seat" + assert response.seatbid.seat.flatten() == [AMX] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and errors and seatnonbid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Alert.general metric shouldn't be updated" + def metrics = pbsService.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) + } + + def "PBS should discard the bid and emit a response warning when request alternate bidder codes are enabled and the allowed bidder codes doesn't match the bidder's request"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode() + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: requestedAllowedBidderCode) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == requestedAllowedBidderCode + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, requestedAllowedBidderCode, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(requestedAllowedBidderCode, AMX, bidRequest.accountId)) + + and: "PBS should emit metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + where: + requestedAllowedBidderCode << [UNKNOWN, BOGUS] + } + + def "PBS should discard the bid and emit a response warning when account alternate bidder codes are enabled and the allowed bidder codes doesn't match the bidder's request"() { + given: "Default bid request with alternate bidder codes" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest) + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: requestedAllowedBidderCode) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == requestedAllowedBidderCode + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, requestedAllowedBidderCode, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsServiceWithAmxBidder.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(requestedAllowedBidderCode, AMX, bidRequest.accountId)) + + and: "PBS should emit metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + where: + requestedAllowedBidderCode << [UNKNOWN, BOGUS] + } + + def "PBS should discard the bid and emit a response warning when default account alternate bidder codes are enabled and the allowed bidder codes doesn't match the bidder's request"() { + given: "Pbs config with default-account-config" + def defaultAccountConfig = AccountConfig.defaultAccountConfig.tap { + alternateBidderCodes = new AlternateBidderCodes().tap { + it.enabled = true + it.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + def pbsConfig = AMX_CONFIG + ["settings.default-account-config": encode(defaultAccountConfig)] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request" + def bidRequest = getBidRequestWithAmxBidder() + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: allowedBidderCodes) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsService) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't seat bid" + assert response.seatbid.isEmpty() + + and: "Response should seatNon bid with code 300" + assert response.ext.seatnonbid.size() == 1 + + def seatNonBid = response.ext.seatnonbid[0] + assert seatNonBid.seat == allowedBidderCodes + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_GENERAL + + and: "Response should contain error" + def error = response.ext?.errors[ErrorType.AMX][0] + assert error.code == 5 + assert error.message == ERROR_BID_CODE_VALIDATION + .formatted(bidResponse.seatbid[0].bid[0].id, allowedBidderCodes, AMX, bidRequest.accountId) + + and: "PBS should emit logs" + def logs = pbsService.getLogsByValue(bidRequest.accountId) + assert logs.contains(INVALID_BIDDER_CODE_LOGS.formatted(allowedBidderCodes, AMX, bidRequest.accountId)) + + and: "Response shouldn't contain demand source" + assert !response?.seatbid?.bid?.ext?.prebid?.meta?.demandSource + + and: "PBS should emit metrics" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + allowedBidderCodes << [BOGUS, UNKNOWN] + } + + def "PBS shouldn't discard bid when hard alias and alternate bidder allow bidder code"() { + given: "PBS config with bidder" + def pbsConfig = AMX_CONFIG + ["adapters.amx.aliases.alias.enabled" : "true", + "adapters.amx.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with alias" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.tap { + amx = null + alias = new Generic() + } + ext.prebid.alternateBidderCodes.bidders = [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [GENERIC] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't discard bid when alternate bidder code allow and soft alias with case"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.aliasUpperCase = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + ext.prebid.alternateBidderCodes = requestAlternateBidderCode + } + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes = accountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.seat == [GENERIC] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(ALIAS)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + requestAlternateBidderCode | accountAlternateBidderCodes + new AlternateBidderCodes(enabled: true, bidders: [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) | null + null | new AlternateBidderCodes(enabled: true, bidders: [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) + } + + def "PBS shouldn't discard bid when alternate bidder code allow and soft alias with case with base bidder in alternate bidder code"() { + given: "Default bid request with amx bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.aliasUpperCase = new Generic() + imp[0].ext.prebid.bidder.amx = null + ext.prebid.aliases = [(ALIAS.value): AMX] + ext.prebid.alternateBidderCodes = requestAlternateBidderCode + } + + and: "Save account config into DB with alternate bidder codes" + def account = getAccountWithAlternateBidderCode(bidRequest).tap { + config.alternateBidderCodes = accountAlternateBidderCodes + } + accountDao.save(account) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.seat == [GENERIC] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(ALIAS)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + + where: + requestAlternateBidderCode | accountAlternateBidderCodes + new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) | null + null | new AlternateBidderCodes(enabled: true, bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) + } + + def "PBS should populate adapter code with requested bidder when conflict soft and hard alias and alternate bidder code"() { + given: "PBS config with bidder" + def pbsConfig = AMX_CONFIG + ["adapters.amx.aliases.alias.enabled" : "true", + "adapters.amx.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with amx bidder and targeting" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + imp[0].ext.prebid.bidder.generic = null + it.ext.prebid.aliases = [(ALIAS.value): GENERIC] + it.ext.prebid.alternateBidderCodes.bidders = [(ALIAS): new BidderConfig(enabled: true, allowedBidderCodesLowerCase: [GENERIC])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [GENERIC] + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder"] == GENERIC.value + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS shouldn't emit validation metrics" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should populate two seat bid when different bidder response with same seat"() { + given: "Default bid request with amx and generic bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.bidder.generic = new Generic() + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC, GENERIC].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten().sort() == [AMX, GENERIC].sort() + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Response shouldn't contain repose millis with amx bidder" + assert !response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + } + + def "PBS should return two seat when same bidder response with different bidder code"() { + given: "Default bid request with amx and generic bidder" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp.add(Imp.getDefaultImpression()) + imp[1].ext.prebid.bidder.amx = new Amx() + imp[1].ext.prebid.bidder.generic = null + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC, AMX])] + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + it.seatbid[0].bid[1].ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX, AMX] + + and: "Response should contain bidder amx targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: 'Response targeting should contain generic' + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + } + + def "PBS should populate seat bid from stored bid response when stored bid response and alternate bidder code specified"() { + given: "Default bid request with amx bidder" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: AMX)] + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flash metrics" + flushMetrics(pbsServiceWithAmxBidder) + + when: "PBS processes auction request" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat == [AMX] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request shouldn't be called due to storedBidResponse" + assert !bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + + def "PBS auction allow bidder code when imp stored request and allowed bidder code present"() { + given: "Default bid request" + def bidRequest = getBidRequestWithAmxBidderAndAlternateBidderCode().tap { + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + ext.prebid.alternateBidderCodes.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "Requesting PBS auction" + def response = pbsServiceWithAmxBidder.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert response.seatbid.seat.sort() == [GENERIC].sort() + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain bidder generic targeting" + def targeting = response.seatbid.bid.ext.prebid.targeting.flatten().collectEntries() + assert targeting["hb_pb_${GENERIC}"] + assert targeting["hb_size_${GENERIC}"] + assert targeting["hb_bidder_${GENERIC}"] == GENERIC.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + and: "Bidder request should be called" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Response shouldn't contain warnings and error and seatNonBid" + assert !response.ext?.warnings + assert !response.ext?.errors + assert !response.ext?.seatnonbid + + and: "Response shouldn't contain demand source" + assert !response.seatbid.first.bid.first.ext.prebid.meta.demandSource + + and: "PBS shouldn't emit validation metrics" + def metrics = pbsServiceWithAmxBidder.sendCollectedMetricsRequest() + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(GENERIC)] + assert !metrics[ADAPTER_RESPONSE_VALIDATION_METRICS.formatted(AMX)] + } + + private static Account getAccountWithAlternateBidderCode(BidRequest bidRequest) { + new Account().tap { + it.uuid = bidRequest.accountId + it.config = new AccountConfig(status: ACTIVE, alternateBidderCodes: new AlternateBidderCodes().tap { + it.enabled = true + it.bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + }) + } + } + + private static BidRequest getBidRequestWithAmxBidderAndAlternateBidderCode() { + getBidRequestWithAmxBidder().tap { + it.ext.prebid.alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodesLowerCase: [AMX])] + } + } + } + + private static BidRequest getBidRequestWithAmxBidder() { + BidRequest.defaultBidRequest.tap { + it.imp[0].ext.prebid.bidder.tap { + generic = null + amx = new Amx() + } + ext.prebid.tap { + returnAllBidStatus = true + targeting = new Targeting() + } + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy index 762c55fe879..e376f611e84 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy @@ -13,6 +13,7 @@ import org.prebid.server.functional.model.request.auction.Geo 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.Site import org.prebid.server.functional.model.request.auction.User import org.prebid.server.functional.service.PrebidServerException @@ -333,11 +334,14 @@ class AmpFpdSpec extends BaseSpec { given: "AMP request" def ampRequest = new AmpRequest(tagId: PBSUtils.randomString) + and: "Amp stored request with FPD data" + def fpdSite = Site.rootFPDSite + def fpdUser = User.rootFPDUser def ampStoredRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.tap { data = new ExtRequestPrebidData(bidders: [extRequestPrebidDataBidder]) bidderConfig = [new ExtPrebidBidderConfig(bidders: [prebidBidderConfigBidder], config: new BidderConfig( - ortb2: new BidderConfigOrtb(site: Site.configFPDSite, user: User.configFPDUser)))] + ortb2: new BidderConfigOrtb(site: fpdSite, user: fpdUser)))] } } @@ -350,25 +354,24 @@ class AmpFpdSpec extends BaseSpec { then: "Bidder request should contain certain FPD field from the stored request" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - def ortb2 = ampStoredRequest.ext.prebid.bidderConfig[0].config.ortb2 verifyAll(bidderRequest) { - ortb2.site.name == site.name - ortb2.site.domain == site.domain - ortb2.site.cat == site.cat - ortb2.site.sectionCat == site.sectionCat - ortb2.site.pageCat == site.pageCat - ortb2.site.page == site.page - ortb2.site.ref == site.ref - ortb2.site.search == site.search - ortb2.site.keywords == site.keywords - ortb2.site.ext.data.language == site.ext.data.language - - ortb2.user.yob == user.yob - ortb2.user.gender == user.gender - ortb2.user.keywords == user.keywords - ortb2.user.ext.data.keywords == user.ext.data.keywords - ortb2.user.ext.data.buyeruid == user.ext.data.buyeruid - ortb2.user.ext.data.buyeruids == user.ext.data.buyeruids + it.site.name == fpdSite.name + it.site.domain == fpdSite.domain + it.site.cat == fpdSite.cat + it.site.sectionCat == fpdSite.sectionCat + it.site.pageCat == fpdSite.pageCat + it.site.page == fpdSite.page + it.site.ref == fpdSite.ref + it.site.search == fpdSite.search + it.site.keywords == fpdSite.keywords + it.site.ext.data.language == fpdSite.ext.data.language + + it.user.yob == fpdUser.yob + it.user.gender == fpdUser.gender + it.user.keywords == fpdUser.keywords + it.user.ext.data.keywords == fpdUser.ext.data.keywords + it.user.ext.data.buyeruid == fpdUser.ext.data.buyeruid + it.user.ext.data.buyeruids == fpdUser.ext.data.buyeruids } and: "Bidder request shouldn't contain imp[0].ext.rp" @@ -413,17 +416,18 @@ class AmpFpdSpec extends BaseSpec { } } - def "PBS should fill unknown FPD when unknown FPD data present"() { + def "PBS should ignore any not FPD data value in bidderconfig.config when merging values"() { given: "AMP request" def ampRequest = new AmpRequest(tagId: PBSUtils.randomString) and: "Stored request" - def fpdGeo = Geo.FPDGeo + def fpdSite = Site.rootFPDSite + def fpdUser = User.rootFPDUser def ampStoredRequest = BidRequest.getDefaultBidRequest(SITE).tap { - site = Site.rootFPDSite - user = User.rootFPDUser + site = fpdSite + user = fpdUser ext.prebid.bidderConfig = [new ExtPrebidBidderConfig(bidders: [GENERIC], config: new BidderConfig(ortb2: - new BidderConfigOrtb(user: new User(geo: fpdGeo))))] + new BidderConfigOrtb(user: new User(geo: Geo.FPDGeo), site: new Site(publisher: new Publisher(name: PBSUtils.randomString)))))] } and: "Save stored request in DB" @@ -433,13 +437,32 @@ class AmpFpdSpec extends BaseSpec { when: "PBS processes amp request" defaultPbsService.sendAmpRequest(ampRequest) - then: "Bidder request should contain certain FPD field from the stored request" + then: "Bidder request should contain FPD field from the stored request" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) verifyAll(bidderRequest) { - user.ext.data.geo.country == fpdGeo.country - user.ext.data.geo.zip == fpdGeo.zip - user.geo.country == ampStoredRequest.user.geo.country - user.geo.zip == ampStoredRequest.user.geo.zip + it.site.name == fpdSite.name + it.site.domain == fpdSite.domain + it.site.cat == fpdSite.cat + it.site.sectionCat == fpdSite.sectionCat + it.site.pageCat == fpdSite.pageCat + it.site.page == fpdSite.page + it.site.ref == fpdSite.ref + it.site.search == fpdSite.search + it.site.keywords == fpdSite.keywords + it.site.ext.data.language == fpdSite.ext.data.language + + it.user.yob == fpdUser.yob + it.user.gender == fpdUser.gender + it.user.keywords == fpdUser.keywords + it.user.ext.data.keywords == fpdUser.ext.data.keywords + it.user.ext.data.buyeruid == fpdUser.ext.data.buyeruid + it.user.ext.data.buyeruids == fpdUser.ext.data.buyeruids + } + + and: "Should should ignore any non FPD data" + verifyAll(bidderRequest) { + !it.user.ext.data.geo + !it.site.ext.data.publisher } } 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..47e08555828 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -4,9 +4,12 @@ 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.ConsentedProvidersSettings import org.prebid.server.functional.model.request.auction.DistributionChannel import org.prebid.server.functional.model.request.auction.Site import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.model.request.auction.UserExt import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils @@ -29,7 +32,7 @@ class AmpSpec extends BaseSpec { def response = defaultPbsService.sendAmpRequestRaw(ampRequest) then: "Response header should contain PBS version" - assert response.headers["x-prebid"] == "pbs-java/$PBS_VERSION" + assert response.headers["x-prebid"] == ["pbs-java/$PBS_VERSION"] where: ampRequest || description @@ -56,7 +59,7 @@ class AmpSpec extends BaseSpec { assert exception.responseBody == "Invalid request format: request.${channel.value.toLowerCase()} must not exist in AMP stored requests." where: - channel << [DistributionChannel.APP, DistributionChannel.DOOH] + channel << [DistributionChannel.APP, DistributionChannel.DOOH] } def "PBS should return info from the stored response when it's defined in the stored request"() { @@ -83,8 +86,8 @@ 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_pb"] == getRoundedTargetingValueWithDownPrecision(price) + assert response.targeting["hb_size"] == "${storedAuctionResponse.bid[0].width}x${storedAuctionResponse.bid[0].height}" and: "PBS not send request to bidder" assert bidder.getRequestCount(ampStoredRequest.id) == 0 @@ -107,7 +110,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 +125,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*.width == [ampRequest.w, msW] assert bidderRequest.regs?.gdpr == (ampRequest.gdprApplies ? 1 : 0) } @@ -150,8 +153,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*.width == [ampRequest.ow] } def "PBS should take parameters from the stored request when it's not specified in the request"() { @@ -176,8 +179,83 @@ 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]?.width == ampStoredRequest.imp[0].banner.format[0].width + assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr + } + + def "PBS should pass addtl_consent to user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers"() { + given: "Default amp request with addtlConsent" + def randomAddtlConsent = PBSUtils.randomString + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = randomAddtlConsent + } + + and: "Save storedRequest into DB" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain addtl consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == randomAddtlConsent + assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == randomAddtlConsent + } + + def "PBS should process original user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers when ampRequest doesn't contain addtl_consent"() { + given: "Default amp request with addtlConsent" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = null + } + + and: "Save storedRequest into DB" + def consentProvidersKebabCase = PBSUtils.randomString + def consentProviders = PBSUtils.randomString + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: consentProvidersKebabCase), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: consentProviders))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request should contain requested consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == consentProvidersKebabCase + assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == consentProviders + } + + def "PBS should left user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers empty when addtl_consent and original fields are empty"() { + given: "Default amp request with addtlConsent" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + addtlConsent = null + } + + and: "Save storedRequest into DB" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + user = new User(ext: new UserExt( + consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: null), + consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: null))) + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Bidder request shouldn't contain consent" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert !bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders + assert !bidderRequest.user.ext.consentedProvidersSettings.consentedProviders } } 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..1506e2e0a4d 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy @@ -9,6 +9,7 @@ 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.DeviceExt +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.Renderer import org.prebid.server.functional.model.request.auction.RendererData @@ -42,23 +43,29 @@ import static org.prebid.server.functional.util.SystemProperties.PBS_VERSION class AuctionSpec extends BaseSpec { private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" - private static final boolean CORS_SUPPORT = false + 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, - "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] + private static final Integer DEFAULT_TIMEOUT = getRandomTimeout() + private static final Integer MIN_BID_ID_LENGTH = 17 + private static final Integer DEFAULT_UUID_LENGTH = 36 private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] @Shared PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) + private static final String IMPS_REQUESTED_METRIC = 'imps_requested' + private static final String IMPS_DROPPED_METRIC = 'imps_dropped' + private static final Integer IMP_LIMIT = 1 + private static final Map PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String, + "auction.default-timeout-ms": DEFAULT_TIMEOUT as String] + def "PBS should return version in response header for auction request for #description"() { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequestRaw(bidRequest) then: "Response header should contain PBS version" - assert response.headers["x-prebid"] == "pbs-java/$PBS_VERSION" + assert response.headers["x-prebid"] == ["pbs-java/$PBS_VERSION"] where: bidRequest || description @@ -410,12 +417,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 @@ -593,4 +598,290 @@ class AuctionSpec extends BaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert !bidderRequest?.device?.ext?.cdep } + + def "PBS should override short bid.id with random uuid when enforce-random-bid-id is enabled"() { + given: "PBS with enabled generate-bid-id" + def pbsConfig = ['auction.enforce-random-bid-id': 'true'] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + enableEvents() + } + + and: "Default bid response" + def originalBidId = PBSUtils.getRandomString(PBSUtils.getRandomNumber(1, MIN_BID_ID_LENGTH - 1)) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.id = originalBidId + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account in DB" + def account = new Account(uuid: bidRequest.accountId, eventsEnabled: true) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Should include imp from original request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.id.sort() == bidRequest.imp.id.sort() + + and: "Bid response should contain changed bid.id for wins event" + def bidIds = response.seatbid.bid.id.flatten() + def bidResponseEvents = response.seatbid.first.bid.first.ext.prebid.events + assert bidResponseEvents.win.contains("win&b=${bidIds.first}") + assert bidResponseEvents.imp.contains("imp&b=${bidIds.first}") + + and: "BidResponse should contain different bid.id" + assert bidIds.sort() != [originalBidId] + + and: "BidResponse should contain generated UUID" + assert PBSUtils.isUUID(response.seatbid.first.bid.first.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't override short bid.id when enforce-random-bid-id in default or disabled"() { + given: "PBS with disabled generate-bid-id" + def pbsConfig = ["auction.enforce-random-bid-id": enforceRandomBidId] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + enableEvents() + } + + and: "Default bid response" + def originalBidId = PBSUtils.getRandomString(PBSUtils.getRandomNumber(1, MIN_BID_ID_LENGTH)) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.id = originalBidId + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account in DB" + def account = new Account(uuid: bidRequest.accountId, eventsEnabled: true) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Should include imp from original request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.id.sort() == bidRequest.imp.id.sort() + + and: "Bid response should contain changed bid.id for wins event" + def bidResponseEvents = response.seatbid.first.bid.first.ext.prebid.events + assert bidResponseEvents.win.contains("win&b=${originalBidId}") + assert bidResponseEvents.imp.contains("imp&b=${originalBidId}") + + and: "BidResponse should contain original bid.id" + assert response.seatbid.bid.id.flatten().sort() == [originalBidId] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + enforceRandomBidId << [null, 'false'] + } + + def "PBS shouldn't override long enough bid.id with random uuid when enforce-random-bid-id is enabled"() { + given: "PBS with enabled generate-bid-id" + def pbsConfig = ['auction.enforce-random-bid-id': 'true'] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + enableEvents() + } + + and: "Default bid response" + def originalBidId = PBSUtils.getRandomString(PBSUtils.getRandomNumber(MIN_BID_ID_LENGTH, DEFAULT_UUID_LENGTH)) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first.bid.first.id = originalBidId + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account in DB" + def account = new Account(uuid: bidRequest.accountId, eventsEnabled: true) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Should include imp from original request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.id.sort() == bidRequest.imp.id.sort() + + and: "Bid response should contain changed bid.id for wins event" + def bidResponseEvents = response.seatbid.first.bid.first.ext.prebid.events + assert bidResponseEvents.win.contains("win&b=${originalBidId}") + assert bidResponseEvents.imp.contains("imp&b=${originalBidId}") + + and: "BidResponse should contain original bid.id" + assert response.seatbid.bid.id.flatten().sort() == [originalBidId] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should drop extra impressions with warnings when number of impressions exceeds impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "PBS should emit an warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == + ["Only first $IMP_LIMIT impressions were kept due to the limit, " + + "all the subsequent impressions have been dropped for the auction" as String] + + and: "PBS shouldn't emit an error" + assert !response.ext?.errors + + and: "Metrics for imps should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - IMP_LIMIT + assert metrics[IMPS_REQUESTED_METRIC] == IMP_LIMIT + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == IMP_LIMIT + + and: "Bidder request should contain imps according to limit" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == IMP_LIMIT + + where: + accountAuctionConfig << [ + new AccountAuctionConfig(impressionLimit: IMP_LIMIT), + new AccountAuctionConfig(impressionLimitSnakeCase: IMP_LIMIT) + ] + } + + def "PBS shouldn't drop extra impressions when number of impressions equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: bidRequest.imp.size())) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when number of impressions less than or equal to impression-limit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def impressionLimit = bidRequest.imp.size() + 1 + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + } + + def "PBS shouldn't drop extra impressions when impression-limit set to #impressionLimit"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.add(Imp.getDefaultImpression()) + } + + and: "Account in the DB with impression limit config" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain seatNonBid" + assert !response?.ext?.seatnonbid + + and: "Response shouldn't contain warnings and error" + assert !response.ext?.warnings + assert !response.ext?.errors + + and: "Metrics for imps requested should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size() + assert !metrics[IMPS_DROPPED_METRIC] + + and: "Response should contain seat bid" + assert response.seatbid[0].bid.size() == bidRequest.imp.size() + + and: "Bidder request should contain originals imps" + assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size() + + where: + impressionLimit << [null, PBSUtils.randomNegativeNumber, 0] + } } 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..13479030d1d 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,11 @@ 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.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.BidderCall import org.prebid.server.functional.repository.HibernateRepositoryService import org.prebid.server.functional.repository.dao.AccountDao import org.prebid.server.functional.repository.dao.StoredImpDao @@ -15,7 +21,11 @@ import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils import spock.lang.Specification +import java.math.RoundingMode + import static java.math.RoundingMode.DOWN +import static java.math.RoundingMode.HALF_UP +import static java.math.RoundingMode.UP import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.util.SystemProperties.DEFAULT_TIMEOUT @@ -36,8 +46,11 @@ 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 String ALERT_GENERAL = "alerts.general" + 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([:]) + protected static final PrebidServerService defaultPbsService = pbsServiceFactory.getService([:]) def setupSpec() { prebidCache.setResponse() @@ -74,7 +87,40 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { logs.findAll { it.contains(text) } } - protected static String getRoundedTargetingValueWithDefaultPrecision(BigDecimal value) { - "${value.setScale(DEFAULT_TARGETING_PRECISION, DOWN)}0" + protected static String getRoundedTargetingValueWithDownPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, DOWN) + } + + protected static String getRoundedTargetingValueWithHalfUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, HALF_UP) + } + + protected static String getRoundedTargetingValueWithUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, UP) + } + + protected static Map> getRequests(BidResponse bidResponse) { + bidResponse.ext.debug.bidders.collectEntries { bidderName, bidderCalls -> + collectRequestByBidderName(bidderName, bidderCalls) + } + } + + protected static List getMediaTypedBids(BidResponse bidResponse, BidMediaType mediaType) { + bidResponse.seatbid*.bid.collectMany { it }.findAll { it.mediaType == mediaType } + } + + 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) }] + } + + private static GString roundWithDefaultPrecisionAndRoundingType(BigDecimal value, RoundingMode roundingMode) { + "${value.setScale(DEFAULT_TARGETING_PRECISION, roundingMode)}0" } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 20536435161..7da1c89fffb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,22 +1,84 @@ package org.prebid.server.functional.tests + +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.config.AlternateBidderCodes +import org.prebid.server.functional.model.config.BidderConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.Amx +import org.prebid.server.functional.model.request.auction.BidAdjustment import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule 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.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype +import org.prebid.server.functional.model.response.auction.BidExt 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.testcontainers.PbsConfig +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.ACUITYADS +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX 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.BidderName.RUBICON +import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.util.PBSUtils.getRandomDecimal class BidAdjustmentSpec extends BaseSpec { + private static final String WILDCARD = '*' + private static final BigDecimal MIN_ADJUST_VALUE = 0 + private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE + private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final int BID_ADJUST_PRECISION = 4 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + private static final Map AMX_CONFIG = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig + AMX_CONFIG) + } + + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(PbsConfig.currencyConverterConfig + AMX_CONFIG) + } + def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -28,10 +90,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * bidAdjustmentFactor where: @@ -40,7 +102,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should prefer bid price adjustment based on media type when request has per-media-type bid adjustment factors"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def mediaTypeBidAdjustment = bidAdjustmentFactor def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { @@ -54,10 +116,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price * + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * mediaTypeBidAdjustment where: @@ -66,7 +128,7 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should adjust bid price for bidder only when request contains bid adjustment for corresponding bidder"() { given: "Default bid request with bid adjustment" - def bidAdjustment = PBSUtils.randomDecimal + def bidAdjustment = randomDecimal def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap { adjustments = [(adjustmentBidder): bidAdjustment] @@ -78,10 +140,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentBidder << [RUBICON, APPNEXUS] @@ -102,10 +164,10 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should not be adjusted" - assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price where: adjustmentMediaType << [VIDEO, NATIVE] @@ -125,7 +187,7 @@ class BidAdjustmentSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - defaultPbsService.sendAuctionRequest(bidRequest) + pbsService.sendAuctionRequest(bidRequest) then: "PBS should fail the request" def exception = thrown(PrebidServerException) @@ -133,6 +195,1507 @@ class BidAdjustmentSpec extends BaseSpec { assert exception.responseBody.contains("Invalid request format: request.ext.prebid.bidadjustmentfactors.$bidderName.value must be a positive number") where: - bidAdjustmentFactor << [0, PBSUtils.randomNegativeNumber] + bidAdjustmentFactor << [MIN_ADJUST_VALUE, PBSUtils.randomNegativeNumber] + } + + def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder and left original bidderRequest with null floors when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder with specific dealId when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def firstImpPrice = PBSUtils.randomPrice + def secondImpPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + firstRuleType | secondRuleType + MULTIPLIER | CPM + MULTIPLIER | STATIC + MULTIPLIER | MULTIPLIER + CPM | CPM + CPM | STATIC + CPM | MULTIPLIER + STATIC | CPM + STATIC | STATIC + STATIC | MULTIPLIER + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [EUR] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustedBidPrice, bidResponse.cur, currency) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with USD currency" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, currency) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + assert pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | MAX_CPM_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + CPM | MAX_CPM_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MIN_ADJUST_VALUE - 1 | ANY | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | BidRequest.defaultBidRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | BidRequest.defaultAudioRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | BidRequest.defaultNativeRequest + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | BidRequest.defaultNativeRequest + } + + def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + assert pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + assert pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't adjust bid price for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS should adjust bid price for matching bidder and alternate bidder code when request has per-bidder bid adjustment factors"() { + given: "Default bid request with bid adjustment and amx bidder" + def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + it.ext.prebid.tap { + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactor]) + } + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * + bidAdjustmentFactor + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + where: + bidAdjustmentFactor << [0.9, 1.1] + } + + def "PBS should prefer bid price adjustment based on media type and alternate bidder code when request has per-media-type bid adjustment factors"() { + given: "Default bid request with bid adjustment" + def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustmentFactors = new BidAdjustmentFactors().tap { + adjustments = [(GENERIC): randomDecimal] + mediaTypes = [(BANNER): [(GENERIC): bidAdjustmentFactor]] + } + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + } + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * + bidAdjustmentFactor + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + where: + bidAdjustmentFactor << [0.9, 1.1] + } + + def "PBS should prefer bid price adjustment based on media type and alternate bidder code when request has per-media-type bid adjustment factors with soft alias"() { + given: "Default bid request with bid adjustment" + def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { + ext.prebid.aliases = [(ALIAS.value): AMX] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = null + imp[0].ext.prebid.bidder.alias = new Generic() + ext.prebid.tap { + bidAdjustmentFactors = new BidAdjustmentFactors().tap { + adjustments = [(GENERIC): randomDecimal] + mediaTypes = [(BANNER): [(GENERIC): bidAdjustmentFactor]] + } + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])] + } + } + } + + and: "Bid response with bidder code" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + it.seatbid[0].bid[0].ext = new BidExt(bidderCode: GENERIC) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price * + bidAdjustmentFactor + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC.value) + + where: + bidAdjustmentFactor << [0.9, 1.1] + } + + def "PBS shouldn't adjust bid price when bid adjustment rule doesn't match with bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ACUITYADS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ACUITYADS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] + } + + def "PBS should adjust bid price when two bid adjustment rules are compatible"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def dealId = PBSUtils.randomString + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def firstBidAdjustmentRule = new BidAdjustmentRule(amx: [(dealId): [adjustmentRule]]) + def secondBidAdjustmentRule = new BidAdjustmentRule(amx: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): firstBidAdjustmentRule, + (ANY) : secondBidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code and dealId" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + } + + def "PBS should adjust bid price when bid adjustment bidder and bidder code different"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] + } + + def "PBS should adjust bid price when bid adjustment bidder and bidder code same as requested"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(amx: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + } + + def "PBS should adjust bid price when bid adjustment bidder is the same as bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + } + + def "PBS should adjust bid price when bid adjustment wildcard bidder and bidder code specified"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(wildcardBidder: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, BID_ADJUST_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, BID_ADJUST_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } + } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + BidRequest.defaultVideoRequest.tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } } } 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..6ca55f4b7bc --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy @@ -0,0 +1,646 @@ +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.Imp +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 + +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.model.response.auction.MediaType.NATIVE +import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO + +class BidExpResponseSpec extends BaseSpec { + + private static final def BANNER_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_HOST_CACHE = PBSUtils.randomNumber + private static final def BANNER_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def VIDEO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def AUDIO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final def NATIVE_TTL_DEFAULT_CACHE = PBSUtils.randomNumber + private static final Map CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": BANNER_TTL_HOST_CACHE as String, + "cache.video-ttl-seconds" : VIDEO_TTL_HOST_CACHE as String] + private static final Map DEFAULT_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": BANNER_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.video" : VIDEO_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.native": NATIVE_TTL_DEFAULT_CACHE as String, + "cache.default-ttl-seconds.audio" : AUDIO_TTL_DEFAULT_CACHE as String] + private static final Map EMPTY_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": "", + "cache.default-ttl-seconds.video" : "", + "cache.default-ttl-seconds.native": "", + "cache.default-ttl-seconds.audio" : ""] + private static final Map EMPTY_CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": "", + "cache.video-ttl-seconds" : ""] + private static def pbsOnlyHostCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + EMPTY_CACHE_TTL_CONFIG) + private static def pbsEmptyTtlService = pbsServiceFactory.getService(EMPTY_CACHE_TTL_CONFIG + EMPTY_CACHE_TTL_HOST_CONFIG) + private static def pbsHostAndDefaultCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG) + + + 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 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 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 global banner config for banner request"() { + given: "Default bidRequest" + def bidRequest = BidRequest.defaultBidRequest + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [BANNER_TTL_HOST_CACHE] + } + + 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 = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.bid.first.exp == [accountCacheTtl] + } + + def "PBS auction should prioritize bid.exp from the response over all other fields from the request and account config"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = PBSUtils.randomNumber + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response with bid.exp" + def randomExp = PBSUtils.randomNumber + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = randomExp + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp for #mediaType when the response, request, and account config don't include such data"() { + given: "Default bid request with specific imp media type" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response with bid.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 = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction should prioritize imp.exp and resolve bid.exp for #mediaType when request and account config include multiple exp sources"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType).tap { + exp = randomExp + } + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber), + bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + and: "Default bid response without bid.exp" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].exp = null + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has #mediaType as mediaType"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + 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) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response?.seatbid?.first?.bid?.first?.exp + + where: + mediaType << [BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has video as mediaType"() { + given: "Default bid request" + def bidsTtlSeconds = PBSUtils.randomNumber + def vastXmTtlSeconds = bidsTtlSeconds + 1 + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(VIDEO) + + ext.prebid.cache = new PrebidCache( + vastXml: new PrebidCacheSettings(ttlSeconds: vastXmTtlSeconds), + bids: new PrebidCacheSettings(ttlSeconds: bidsTtlSeconds)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == vastXmTtlSeconds + } + + def "PBS auction should resolve bid.exp when ext.prebid.cache.bids.ttlseconds is specified and no higher-priority fields are present"() { + given: "Default bid request" + def randomExp = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: randomExp)) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def auctionConfig = new AccountAuctionConfig( + videoCacheTtl: PBSUtils.randomNumber, + bannerCacheTtl: PBSUtils.randomNumber) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == randomExp + + where: + mediaType << [BANNER, VIDEO, NATIVE, AUDIO] + } + + def "PBS auction shouldn't resolve bid.exp when the account config and request imp type do not match"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType | auctionConfig + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + VIDEO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: null) + BANNER | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + BANNER | new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + NATIVE | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber) + AUDIO | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber) + } + + def "PBS auction shouldn't resolve bid.exp when account config and request imp type match but account config for cache-ttl is not specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: null))) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [VIDEO, BANNER, NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when account.auction.{banner/video}-cache-ttl and banner bid specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountAuctionConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == accountCacheTtl + + where: + mediaType | accountCacheTtl | accountAuctionConfig + BANNER | PBSUtils.randomNumber | new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl) + VIDEO | PBSUtils.randomNumber | new AccountAuctionConfig(videoCacheTtl: accountCacheTtl) + } + + def "PBS auction should resolve bid.exp when cache.{banner/video}-ttl-seconds config specified"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + enableCache() + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == expValue + + where: + mediaType | expValue + BANNER | BANNER_TTL_HOST_CACHE + VIDEO | VIDEO_TTL_HOST_CACHE + } + + def "PBS auction shouldn't resolve bid.exp when cache ttl-seconds is specified for #mediaType mediaType request"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber)) + } + + when: "PBS processes auction request" + def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest) + + then: "Bid response shouldn't contain exp data" + assert !response.seatbid.first.bid.first.exp + + where: + mediaType << [NATIVE, AUDIO] + } + + def "PBS auction should resolve bid.exp when cache.default-ttl-seconds.{banner,video,audio,native} is specified and no higher-priority fields are present"() { + given: "Prebid server with empty host config and default cache ttl config" + def config = EMPTY_CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG + def prebidServerService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0] = Imp.getDefaultImpression(mediaType) + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain exp data" + assert response.seatbid.first.bid.first.exp == bidExpValue + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) + + where: + mediaType | bidExpValue + BANNER | BANNER_TTL_DEFAULT_CACHE + VIDEO | VIDEO_TTL_DEFAULT_CACHE + AUDIO | AUDIO_TTL_DEFAULT_CACHE + NATIVE | NATIVE_TTL_DEFAULT_CACHE + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy new file mode 100644 index 00000000000..15095aaa3e8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy @@ -0,0 +1,111 @@ +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.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.request.auction.BidRounding.DOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.TRUE +import static org.prebid.server.functional.model.request.auction.BidRounding.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.UP + +class BidRoundingSpec extends BaseSpec { + + def "PBS should round bid value to the down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.randomFloorValue + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithDownPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: null), + new AccountAuctionConfig(bidRounding: UNKNOWN), + new AccountAuctionConfig(bidRounding: DOWN), + new AccountAuctionConfig(bidRoundingSnakeCase: DOWN)] + } + + def "PBS should round bid value to the up when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: UP), + new AccountAuctionConfig(bidRoundingSnakeCase: UP)] + } + + def "PBS should round bid value to the up or down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithHalfUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: TRUE), + new AccountAuctionConfig(bidRoundingSnakeCase: TRUE)] + } + + private static final Account getAccountWithBidRounding(String accountId, AccountAuctionConfig accountAuctionConfig) { + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) + } +} 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..9dd17c31e0a 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy @@ -18,6 +18,8 @@ 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.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH import static org.prebid.server.functional.util.HttpUtil.REFERER_HEADER @@ -61,7 +63,7 @@ class BidValidationSpec extends BaseSpec { and: "Bid validation metric value is incremented" def metrics = strictPrebidService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 where: bidRequest << [BidRequest.getDefaultBidRequest(DistributionChannel.APP).tap { @@ -103,7 +105,7 @@ class BidValidationSpec extends BaseSpec { and: "Bid validation metric value is incremented" def metrics = softPrebidService.sendCollectedMetricsRequest() - assert metrics["alerts.general"] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "PBS log should contain message" def logs = softPrebidService.getLogsByTime(startTime) @@ -133,7 +135,7 @@ class BidValidationSpec extends BaseSpec { dooh.id = null dooh.venueType = null } - bidDoohRequest.ext.prebid.debug = 1 + bidDoohRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidDoohRequest) @@ -148,7 +150,7 @@ class BidValidationSpec extends BaseSpec { given: "Default basic BidRequest" def bidRequest = BidRequest.defaultBidRequest bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -159,9 +161,9 @@ class BidValidationSpec extends BaseSpec { } def "PBS should treat bids with 0 price as valid when deal id is present"() { - given: "Default basic BidRequest with generic bidder" + given: "Default basic BidRequest with generic bidder and enabled debug" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Bid response with 0 price bid" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) @@ -183,16 +185,21 @@ class BidValidationSpec extends BaseSpec { } def "PBS should drop invalid bid and emit debug error when bid price is #bidPrice and deal id is #dealId"() { - given: "Default basic BidRequest with generic bidder" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + given: "Default basic BidRequest with generic bidder and enabled debug" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.ext.prebid.debug = debug + it.test = test + } and: "Bid response" - def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) - def bid = bidResponse.seatbid.first().bid.first() - bid.dealid = dealId - bid.price = bidPrice - def bidId = bid.id + def bidId = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + id = bidId + dealid = dealId + price = bidPrice + } + } and: "Set bidder response" bidder.setResponse(bidRequest.id, bidResponse) @@ -201,13 +208,61 @@ class BidValidationSpec extends BaseSpec { def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Invalid bid should be deleted" - assert response.seatbid.size() == 0 + assert !response.seatbid + assert !response.ext.seatnonbid and: "PBS should emit an error" assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$bidId'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | null + DISABLED | ENABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + DISABLED | ENABLED | 0 | null + DISABLED | ENABLED | null | PBSUtils.randomNumber + DISABLED | ENABLED | null | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | null + ENABLED | DISABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + ENABLED | DISABLED | 0 | null + ENABLED | DISABLED | null | PBSUtils.randomNumber + ENABLED | DISABLED | null | null + } + + def "PBS should drop invalid bid without debug error when request debug disabled and bid price is #bidPrice and deal id is #dealId"() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.debug = DISABLED + } + + and: "Bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid.first.tap { + dealid = dealId + price = bidPrice + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Invalid bid should be deleted" + assert !response.seatbid + assert !response.ext.seatnonbid + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + + and: "PBS should call bidder" + def bidderRequests = bidder.getBidderRequests(bidResponse.id) + assert bidderRequests.size() == 1 + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -220,7 +275,7 @@ class BidValidationSpec extends BaseSpec { def "PBS should only drop invalid bid without discarding whole seat"() { given: "Default basic BidRequest with generic bidder" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED bidRequest.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] and: "Bid response with 2 bids" @@ -239,7 +294,7 @@ class BidValidationSpec extends BaseSpec { when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) - then: "Invalid bids should be deleted" + then: "Bid response contains only valid bid" assert response.seatbid?.first()?.bid*.id == [validBidId] and: "PBS should emit an error" @@ -247,6 +302,53 @@ class BidValidationSpec extends BaseSpec { assert response.ext?.warnings[ErrorType.PREBID]*.message == ["Dropped bid '$invalidBid.id'. Does not contain a positive (or zero if there is a deal) 'price'" as String] + where: + debug | test | bidPrice | dealId + 0 | 1 | PBSUtils.randomNegativeNumber | null + 0 | 1 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 0 | 1 | 0 | null + 0 | 1 | null | PBSUtils.randomNumber + 0 | 1 | null | null + 1 | 0 | PBSUtils.randomNegativeNumber | null + 1 | 0 | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber + 1 | 0 | 0 | null + 1 | 0 | null | PBSUtils.randomNumber + 1 | 0 | null | null + } + + def "PBS should only drop invalid bid without discarding whole seat without debug error when request debug disabled "() { + given: "Default basic BidRequest with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + test = DISABLED + ext.prebid.tap { + debug = DISABLED + multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)] + } + } + + and: "Bid response with 2 bids" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidResponse.seatbid[0].bid << Bid.getDefaultBid(bidRequest.imp.first()) + + and: "One of the bids is invalid" + def invalidBid = bidResponse.seatbid.first().bid.first() + invalidBid.dealid = dealId + invalidBid.price = bidPrice + def validBidId = bidResponse.seatbid.first().bid.last().id + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response contains only valid bid" + assert response.seatbid?.first()?.bid*.id == [validBidId] + + and: "PBS shouldn't emit an error" + assert !response.ext?.warnings + assert !response.ext?.warnings + where: bidPrice | dealId PBSUtils.randomNegativeNumber | null @@ -257,10 +359,7 @@ class BidValidationSpec extends BaseSpec { } def "PBS should update 'adapter.generic.requests.bid_validation' metric when bid validation error appears"() { - given: "Initial 'adapter.generic.requests.bid_validation' metric value" - def initialMetricValue = getCurrentMetricValue(defaultPbsService, "adapter.generic.requests.bid_validation") - - and: "Bid request" + given: "Bid request" def bidRequest = BidRequest.defaultBidRequest and: "Set invalid bid response" @@ -269,12 +368,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..b47a2e47ddf --- /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 width and height is valid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(width: bannerFormatWidth, 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].width == bannerFormatWidth + assert bidderRequest?.imp[0]?.banner?.format[0].height == bannerFormatHeight + + where: + bannerFormatWidth | 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 width or height is invalid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(width: bannerFormatWidth, 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: + bannerFormatWidth | 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 width and height is invalid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner.format = [new Format(width: bannerFormatWidth, 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: + bannerFormatWidth | bannerFormatHeight + 0 | 0 + 0 | null + 0 | PBSUtils.randomNegativeNumber + null | null + null | PBSUtils.randomNegativeNumber + PBSUtils.randomNegativeNumber | PBSUtils.randomNegativeNumber + } + + def "PBS should successfully pass when banner width and height is valid"() { + given: "Default bid request with banner format" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].banner = new Banner(width: bannerFormatWidth, 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?.width == bannerFormatWidth + assert bidderRequest?.imp[0]?.banner?.height == bannerFormatHeight + + where: + bannerFormatWidth | 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(width: bannerFormatWidth, 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: + bannerFormatWidth | 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(width: 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.width = responseWidth + 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='${responseWidth}x${responseHeight}'" + + and: "Bid response should contain width and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.width == responseWidth + 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 | responseWidth | 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(width: 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.width = responseWidth + 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 width and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.width == responseWidth + 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 | responseWidth | 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(width: 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.width = responseWidth + 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='${responseWidth}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 | responseWidth | 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(width: 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 { + width = 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 width and height from stored response" + def bid = bidResponse.seatbid[0].bid[0] + assert bid.width == 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(width: 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 { + width = responseWidth + 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 | responseWidth | 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(width: 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.width = responseWidth + 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='${responseWidth}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 | responseWidth | 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..220585f008f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidderParamsSpec.groovy @@ -1,42 +1,70 @@ package org.prebid.server.functional.tests -import io.qameta.allure.Issue +import org.prebid.server.functional.model.bidder.AppNexus +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 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.Adrino +import org.prebid.server.functional.model.request.auction.Amx +import org.prebid.server.functional.model.request.auction.AuctionEnvironment 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.InterestGroupAuctionSupport import org.prebid.server.functional.model.request.auction.Native +import org.prebid.server.functional.model.request.auction.PrebidOptions 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.auction.Source +import org.prebid.server.functional.model.request.auction.Targeting 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.BidExt 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.ALIAS_UPPER_CASE +import static org.prebid.server.functional.model.bidder.BidderName.AMX 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.BidderName.GENERIC_CAMEL_CASE +import static org.prebid.server.functional.model.bidder.BidderName.OPENX 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.AuctionEnvironment.DEVICE_ORCHESTRATED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.NOT_SUPPORTED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.SERVER_ORCHESTRATED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.UNKNOWN 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,11 +81,14 @@ 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 + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(adapterConfig) + where: adapterDefault | generic | adapterConfig "true" | "true" | ["adapter-defaults.enabled" : adapterDefault, @@ -79,7 +110,10 @@ 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] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(adapterConfig) where: adapterDefault | generic | adapterConfig @@ -92,8 +126,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should modify vast xml when adapter-defaults.modifying-vast-xml-allowed = #adapterDefault and BIDDER.modifying-vast-xml-allowed = #generic"() { given: "PBS with adapter configuration" - def pbsService = pbsServiceFactory.getService(["adapter-defaults.modifying-vast-xml-allowed": adapterDefault, - "adapters.generic.modifying-vast-xml-allowed": generic]) + def pbsConfig = ["adapter-defaults.modifying-vast-xml-allowed": adapterDefault, + "adapters.generic.modifying-vast-xml-allowed": generic] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default vtrack request" String payload = PBSUtils.randomString @@ -105,13 +140,16 @@ class BidderParamsSpec extends BaseSpec { accountDao.save(account) when: "PBS processes vtrack request" - pbsService.sendVtrackRequest(request, accountId.toString()) + pbsService.sendPostVtrackRequest(request, accountId.toString()) then: "vast xml is modified" def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) assert prebidCacheRequest.size() == 1 assert prebidCacheRequest.first().contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: adapterDefault | generic "true" | "true" @@ -120,8 +158,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should not modify vast xml when adapter-defaults.modifying-vast-xml-allowed = #adapterDefault and BIDDER.modifying-vast-xml-allowed = #generic"() { given: "PBS with adapter configuration" - def pbsService = pbsServiceFactory.getService(["adapter-defaults.modifying-vast-xml-allowed": adapterDefault, - "adapters.generic.modifying-vast-xml-allowed": generic]) + def pbsConfig = ["adapter-defaults.modifying-vast-xml-allowed": adapterDefault, + "adapters.generic.modifying-vast-xml-allowed": generic] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default VtrackRequest" String payload = PBSUtils.randomString @@ -133,13 +172,16 @@ class BidderParamsSpec extends BaseSpec { accountDao.save(account) when: "PBS processes vtrack request" - pbsService.sendVtrackRequest(request, accountId.toString()) + pbsService.sendPostVtrackRequest(request, accountId.toString()) then: "vast xml is not modified" def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload) assert prebidCacheRequest.size() == 1 assert !prebidCacheRequest.first().contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: adapterDefault | generic "true" | "false" @@ -148,13 +190,14 @@ class BidderParamsSpec extends BaseSpec { def "PBS should mask values when adapter-defaults.pbs-enforces-ccpa = #adapterDefault settings when BIDDER.pbs-enforces-ccpa = #generic"() { given: "PBS with adapter configuration" - def pbsService = pbsServiceFactory.getService(["adapter-defaults.pbs-enforces-ccpa": adapterDefault, - "adapters.generic.pbs-enforces-ccpa": generic]) + def pbsConfig = ["adapter-defaults.pbs-enforces-ccpa": adapterDefault, + "adapters.generic.pbs-enforces-ccpa": generic] + def pbsService = pbsServiceFactory.getService(pbsConfig) 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)) @@ -167,6 +210,9 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequests.device?.geo?.lat as BigDecimal == PBSUtils.roundDecimal(lat, 2) assert bidderRequests.device?.geo?.lon as BigDecimal == PBSUtils.roundDecimal(lon, 2) + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: adapterDefault | generic "true" | "true" @@ -175,13 +221,14 @@ class BidderParamsSpec extends BaseSpec { def "PBS should not mask values when adapter-defaults.pbs-enforces-ccpa = #adapterDefault settings when BIDDER.pbs-enforces-ccpa = #generic"() { given: "PBS with adapter configuration" - def pbsService = pbsServiceFactory.getService(["adapter-defaults.pbs-enforces-ccpa": adapterDefault, - "adapters.generic.pbs-enforces-ccpa": generic]) + def pbsConfig = ["adapter-defaults.pbs-enforces-ccpa": adapterDefault, + "adapters.generic.pbs-enforces-ccpa": generic] + def pbsService = pbsServiceFactory.getService(pbsConfig) 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)) @@ -194,6 +241,9 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequests.device?.geo?.lat == lat assert bidderRequests.device?.geo?.lon == lon + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: adapterDefault | generic "true" | "false" @@ -207,7 +257,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 +292,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,12 +321,12 @@ 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", - "adapters.generic.endpoint" : "https://", - "http-client.circuit-breaker.enabled": "false"]) + def pbsConfig = ["adapters.generic.enabled" : "true", + "adapters.generic.endpoint" : "https://", + "http-client.circuit-breaker.enabled": "false"] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest @@ -285,8 +335,11 @@ 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"] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should reject bidder when bidder params from request doesn't satisfy json-schema for auction request"() { @@ -380,9 +433,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should emit error when filter-imp-media-type = true and #configMediaType is empty in bidder config"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - ("adapters.generic.meta-info.${configMediaType}".toString()): ""]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + ("adapters.generic.meta-info.${configMediaType}".toString()): ""] + def pbsService = pbsServiceFactory.getService(pbsConfig) when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -391,8 +444,11 @@ 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."] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) where: configMediaType | bidRequest @@ -402,9 +458,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should not validate request when filter-imp-media-type = false and #configMediaType is empty in bidder config"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "false", - ("adapters.generic.meta-info.${configMediaType}".toString()): ""]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "false", + ("adapters.generic.meta-info.${configMediaType}".toString()): ""] + def pbsService = pbsServiceFactory.getService(pbsConfig) when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -415,6 +471,9 @@ class BidderParamsSpec extends BaseSpec { and: "Response should not contain error" assert !response.ext?.errors + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: configMediaType | bidRequest "app-media-types" | BidRequest.getDefaultBidRequest(APP) @@ -423,9 +482,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should emit error when filter-imp-media-type = true and request contains media type that is not configured in bidder config"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - "adapters.generic.meta-info.site-media-types": "native"]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + "adapters.generic.meta-info.site-media-types": "native"] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default basic BidRequest with banner, native" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -450,13 +509,16 @@ class BidderParamsSpec extends BaseSpec { and: "Response should not contain warnings" assert !response.ext?.warnings + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should not validate request when filter-imp-media-type = false and request contains only media type that is not configured in bidder config"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "false", - "adapters.generic.meta-info.site-media-types": "native"]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "false", + "adapters.generic.meta-info.site-media-types": "native"] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default basic BidRequest with banner, native" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -475,13 +537,16 @@ class BidderParamsSpec extends BaseSpec { and: "Response should not contain error" assert !response.ext?.errors + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should emit error for request with multiple impressions when filter-imp-media-type = true, one of imp doesn't contain supported media type"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - "adapters.generic.meta-info.site-media-types": "native,video"]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + "adapters.generic.meta-info.site-media-types": "native,video"] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default basic BidRequest with banner, native" def nativeImp = Imp.getDefaultImpression(NATIVE) @@ -508,26 +573,29 @@ 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] and: "seatbid should not be empty" assert !response.seatbid.isEmpty() + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS auction should reject the bidder with media-type that is not supported by DOOH configuration with proper warning"() { given: "PBS service with configuration for dooh media-types" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - "adapters.generic.meta-info.dooh-media-types": mediaType]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + "adapters.generic.meta-info.dooh-media-types": mediaType] + def pbsService = pbsServiceFactory.getService(pbsConfig) when: "Requesting PBS auction" 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 @@ -535,6 +603,9 @@ class BidderParamsSpec extends BaseSpec { and: "Should't send any bidder request" assert !bidder.getBidderRequests(bidRequest.id) + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: mediaType | bidRequest VIDEO.value | BidRequest.getDefaultBidRequest(DOOH) @@ -546,9 +617,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS auction should reject only imps with media-type that is not supported by DOOH configuration with proper warning"() { given: "PBS service with configuration for dooh media-types" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - "adapters.generic.meta-info.dooh-media-types": mediaType.value]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + "adapters.generic.meta-info.dooh-media-types": mediaType.value] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default bid response with adm and nurl" def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -561,8 +632,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 @@ -570,6 +641,9 @@ class BidderParamsSpec extends BaseSpec { and: "Should send bidder request with only proper imp" assert bidder.getBidderRequest(bidRequest.id).imp.id == [bidRequest.imp.first().id] + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: mediaType | bidRequest BANNER | BidRequest.getDefaultBidRequest(DOOH).tap { imp << Imp.getDefaultImpression(VIDEO) } @@ -579,9 +653,9 @@ class BidderParamsSpec extends BaseSpec { def "PBS should return empty seatBit when filter-imp-media-type = true, request.imp doesn't contain supported media type"() { given: "Pbs config" - def pbsService = pbsServiceFactory.getService( - ["auction.filter-imp-media-type.enabled" : "true", - "adapters.generic.meta-info.site-media-types": "native,video"]) + def pbsConfig = ["auction.filter-imp-media-type.enabled" : "true", + "adapters.generic.meta-info.site-media-types": "native,video"] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default basic BidRequest with banner" def bidRequest = BidRequest.defaultBidRequest.tap { @@ -596,14 +670,17 @@ 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."] and: "seatbid should be empty" assert response.seatbid.isEmpty() + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should send server specific info to bidder when such is set in PBS config"() { @@ -611,9 +688,10 @@ class BidderParamsSpec extends BaseSpec { def serverDataCenter = PBSUtils.randomString def serverExternalUrl = "https://${PBSUtils.randomString}.com/" def serverHostVendorId = PBSUtils.randomNumber - def pbsService = pbsServiceFactory.getService(["datacenter-region" : serverDataCenter, - "external-url" : serverExternalUrl as String, - "gdpr.host-vendor-id": serverHostVendorId as String]) + def pbsConfig = ["datacenter-region" : serverDataCenter, + "external-url" : serverExternalUrl as String, + "gdpr.host-vendor-id": serverHostVendorId as String] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Bid request" def bidRequest = BidRequest.defaultBidRequest @@ -627,13 +705,17 @@ class BidderParamsSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.server?.externalUrl == serverExternalUrl assert bidderRequest.ext.prebid.server.datacenter == serverDataCenter assert bidderRequest.ext.prebid.server.gvlId == serverHostVendorId + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should request to bidder with header Content-Encoding = gzip when adapters.BIDDER.endpoint-compression = gzip"() { given: "PBS with adapter configuration" def compressionType = GZIP.value - def pbsService = pbsServiceFactory.getService(["adapters.generic.enabled" : "true", - "adapters.generic.endpoint-compression": compressionType]) + def pbsConfig = ["adapters.generic.enabled" : "true", + "adapters.generic.endpoint-compression": compressionType] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default bid request" def bidRequest = BidRequest.defaultBidRequest @@ -642,14 +724,18 @@ 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should send request to bidder without header Content-Encoding when adapters.BIDDER.endpoint-compression = none"() { given: "PBS with adapter configuration" - def pbsService = pbsServiceFactory.getService(["adapters.generic.enabled" : "true", - "adapters.generic.endpoint-compression": NONE.value]) + def pbsConfig = ["adapters.generic.enabled" : "true", + "adapters.generic.endpoint-compression": NONE.value] + def pbsService = pbsServiceFactory.getService(pbsConfig) and: "Default bid request" def bidRequest = BidRequest.defaultBidRequest @@ -658,8 +744,11 @@ 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) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should not treat reserved imp[].ext.tid object as a bidder"() { @@ -699,9 +788,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 +808,986 @@ 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 should emit warning and proceed auction when ext.prebid fields include adunitcode"() { + given: "Default bid request with populated ext.prebid.bidderParams" + def genericBidderParams = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.bidderParams = [adUnitCode : PBSUtils.randomString, + (GENERIC.value): genericBidderParams] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain error" + assert !response.ext?.errors + + 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.adUnitCode was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder contains unknown bidder: adUnitCode"] + } + + 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 + prebid.adUnitCode = PBSUtils.randomString + generic = new Generic() + auctionEnvironment = PBSUtils.getRandomEnum(AuctionEnvironment, [AuctionEnvironment.SERVER_ORCHESTRATED, AuctionEnvironment.UNKNOWN]) + 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" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "Bidder request should contain same field as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.imp[0].ext) { + it.bidder == impExt.generic + it.auctionEnvironment == impExt.auctionEnvironment + it.all == impExt.all + it.context == impExt.context + it.data == impExt.data + it.general == impExt.general + it.gpid == impExt.gpid + it.skadn == impExt.skadn + it.tid == impExt.tid + it.prebid.adUnitCode == impExt.prebid.adUnitCode + } + } + + def "PBS shouldn't emit warning and proceed auction when all imp.ext.prebid fields known for PBS"() { + given: "PBS with old ortb version" + def pbsConfig = ['adapters.generic.ortb-version': '2.5'] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with populated imp.ext.prebid" + def impExt = ImpExt.getDefaultImpExt().tap { + prebid.adUnitCode = PBSUtils.randomString + prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + prebid.isRewardedInventory = PBSUtils.getRandomNumber(0, 1) + prebid.options = new PrebidOptions(echoVideoAttrs: PBSUtils.randomBoolean) + } + def bidRequest = BidRequest.defaultVideoRequest.tap { + imp[0].rwdd = null + imp[0].ext = impExt + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + 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 error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "Bidder request should contain same field as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.imp[0].ext.prebid) { + it.adUnitCode == impExt.prebid.adUnitCode + it.storedRequest == impExt.prebid.storedRequest + it.isRewardedInventory == impExt.prebid.isRewardedInventory + it.options.echoVideoAttrs == impExt.prebid.options.echoVideoAttrs + } + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should proceed auction without warning when all ext.prebid.bidderParams fields are known"() { + given: "Default bid request with populated ext.prebid.bidderParams" + def genericBidderParams = PBSUtils.randomString + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.bidderParams = [ae : PBSUtils.randomString, + all : PBSUtils.randomString, + context : PBSUtils.randomString, + data : PBSUtils.randomString, + general : PBSUtils.randomString, + gpid : PBSUtils.randomString, + skadn : PBSUtils.randomString, + tid : PBSUtils.randomString, + (GENERIC.value): genericBidderParams + ] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response shouldn't contain error" + assert !response.ext?.errors + + and: "Response shouldn't contain warning" + assert !response.ext?.warnings + + and: "Bidder request should bidderParams only for bidder" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.ext.prebid.bidderParams == [(GENERIC.value): genericBidderParams] + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsConfig = ['adapters.generic.meta-info.currency-accepted': ''] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should send request to bidder when adapters.bidder.aliases.bidder.meta-info.currency-accepted not specified"() { + given: "PBS with adapter configuration" + def pbsConfig = [ + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.meta-info.currency-accepted": ""] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should send request to bidder when adapters.bidder.meta-info.currency-accepted intersect with requested currency"() { + given: "PBS with adapter configuration" + def pbsConfig = ["adapters.generic.meta-info.currency-accepted": "${USD},${EUR}".toString()] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + 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 pbsConfig = ["adapters.generic.meta-info.currency-accepted": "${JPY},${CHF}".toString()] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + 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 pbsConfig = [ + "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()] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + 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 pbsConfig = [ + "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()] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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 + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should add auction environment to imp.ext.igs when it is present in imp.ext and imp.ext.igs is empty"() { + given: "Default bid request with populated imp.ext" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.tap { + auctionEnvironment = requestedAuctionEnvironment + interestGroupAuctionSupports = new InterestGroupAuctionSupport(auctionEnvironment: null) + } + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should imp[].{ae/ext.igs.ae} same value as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.auctionEnvironment == requestedAuctionEnvironment + assert bidderRequest.imp[0].ext.interestGroupAuctionSupports.auctionEnvironment == requestedAuctionEnvironment + + where: + requestedAuctionEnvironment << [NOT_SUPPORTED, DEVICE_ORCHESTRATED] + } + + def "PBS shouldn't add unsupported auction environment to imp.ext.igs when it is present in imp.ext and imp.ext.igs is empty"() { + given: "Default bid request with populated imp.ext" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.tap { + auctionEnvironment = requestedAuctionEnvironment + interestGroupAuctionSupports = new InterestGroupAuctionSupport(auctionEnvironment: null) + } + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should imp[].ae same value as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.auctionEnvironment == requestedAuctionEnvironment + assert !bidderRequest.imp[0].ext.interestGroupAuctionSupports.auctionEnvironment + + where: + requestedAuctionEnvironment << [SERVER_ORCHESTRATED, UNKNOWN] + } + + def "PBS shouldn't change auction environment in imp.ext.igs when it is present in both imp.ext and imp.ext.igs"() { + given: "Default bid request with populated imp.ext" + def extAuctionEnv = PBSUtils.getRandomEnum(AuctionEnvironment, [SERVER_ORCHESTRATED, UNKNOWN]) + def extIgsAuctionEnv = PBSUtils.getRandomEnum(AuctionEnvironment, [SERVER_ORCHESTRATED, UNKNOWN]) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.tap { + auctionEnvironment = extAuctionEnv + interestGroupAuctionSupports = new InterestGroupAuctionSupport(auctionEnvironment: extIgsAuctionEnv) + } + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should imp[].{ae/ext.igs.ae} same value as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.auctionEnvironment == extAuctionEnv + assert bidderRequest.imp[0].ext.interestGroupAuctionSupports.auctionEnvironment == extIgsAuctionEnv + } + + def "PBS should reject alias bidders when bidder params from request doesn't satisfy own json-schema"() { + given: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + it.generic.exampleProperty = PBSUtils.randomNumber + //Adrino hard coded bidder alias in generic.yaml + it.adrino = new Adrino(hash: PBSUtils.randomNumber) + } + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder should be dropped" + assert response.ext?.warnings[PREBID]*.code == [999, 999, 999] + assert response.ext?.warnings[PREBID]*.message == + ["WARNING: request.imp[0].ext.prebid.bidder.generic was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder.generic failed validation.\n" + + "\$.exampleProperty: integer found, string expected", + "WARNING: request.imp[0].ext.prebid.bidder.adrino was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder.adrino failed validation.\n" + + "\$.hash: integer found, string expected", + "WARNING: request.imp[0].ext must contain at least one valid bidder"] + + and: "PBS should not call bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + + and: "targeting should be empty" + assert response.seatbid.isEmpty() + } + + def "PBS should reject alias bidders when bidder params from request doesn't satisfy aliased json-schema"() { + given: "Default basic generic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + it.generic.exampleProperty = PBSUtils.randomNumber + //Nativo hard coded bidder alias in generic.yaml + it.nativo = new Generic(exampleProperty: PBSUtils.randomNumber) + } + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder should be dropped" + assert response.ext?.warnings[PREBID]*.code == [999, 999, 999] + assert response.ext?.warnings[PREBID]*.message == + ["WARNING: request.imp[0].ext.prebid.bidder.generic was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder.generic failed validation.\n" + + "\$.exampleProperty: integer found, string expected", + "WARNING: request.imp[0].ext.prebid.bidder.nativo was dropped with a reason: " + + "request.imp[0].ext.prebid.bidder.nativo failed validation.\n" + + "\$.exampleProperty: integer found, string expected", + "WARNING: request.imp[0].ext must contain at least one valid bidder"] + + and: "PBS should not call bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + + and: "targeting should be empty" + assert response.seatbid.isEmpty() + } + + def "PBS should send bidder code from imp[].ext.prebid.bidder to seatbid.bid.ext.prebid.meta.adapterCode"() { + given: "Default basic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].seat = OPENX + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.GENERIC] + + and: "Bidder request should be valid" + assert bidder.getBidderRequest(bidRequest.id) + } + + def "PBS should send bidder code from imp[].ext.prebid.bidder to seatbid.bid.ext.prebid.meta.adapterCode when requested soft alias"() { + given: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + generic = null + alias = new Generic() + } + ext.prebid.aliases = [(ALIAS.value): BidderName.GENERIC] + ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.GENERIC] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [BidderName.ALIAS] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${BidderName.ALIAS}"] + assert targeting["hb_size_${BidderName.ALIAS}"] + assert targeting["hb_bidder"] == BidderName.ALIAS.value + assert targeting["hb_bidder_${BidderName.ALIAS}"] == BidderName.ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + } + + def "PBS should populate same code for adapter code when make call for generic hard code alias"() { + given: "PBS config with bidder" + def pbsConfig = ["adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + generic = null + alias = new Generic() + } + ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [BidderName.ALIAS] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${BidderName.ALIAS}"] + assert targeting["hb_size_${BidderName.ALIAS}"] + assert targeting["hb_bidder"] == BidderName.ALIAS.value + assert targeting["hb_bidder_${BidderName.ALIAS}"] == BidderName.ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should make call for alias when hard alias and demandSource specified"() { + given: "PBS config with bidder" + def pbsConfig = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.amx.aliases.alias.enabled" : "true", + "adapters.amx.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid Request with generic and openx bidder within separate imps" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + generic = null + alias = new Generic() + } + ext.prebid.targeting = new Targeting() + } + + and: "Bid response with bidder code" + def demandSource = PBSUtils.randomString + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, BidderName.ALIAS).tap { + it.seatbid[0].bid[0].ext = new BidExt(demandSource: demandSource) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain demand source" + assert response.seatbid.bid.ext.prebid.meta.demandSource.flatten() == [demandSource] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [BidderName.ALIAS] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${BidderName.ALIAS}"] + assert targeting["hb_size_${BidderName.ALIAS}"] + assert targeting["hb_bidder"] == BidderName.ALIAS.value + assert targeting["hb_bidder_${BidderName.ALIAS}"] == BidderName.ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should send bidder code from imp[].ext.prebid.bidder to seatbid.bid.ext.prebid.meta.adapterCode when requested soft alias with upper case"() { + given: "Default bid request with alias" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.aliases = [(ALIAS.value): BidderName.GENERIC] + ext.prebid.targeting = new Targeting() + imp[0].ext.prebid.bidder.tap { + generic = null + aliasUpperCase = new Generic() + } + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.GENERIC] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [ALIAS_UPPER_CASE] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${ALIAS_UPPER_CASE}"] + assert targeting["hb_size_${ALIAS_UPPER_CASE}"] + assert targeting["hb_bidder"] == ALIAS_UPPER_CASE.value + assert targeting["hb_bidder_${ALIAS_UPPER_CASE}"] == ALIAS_UPPER_CASE.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS_UPPER_CASE.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + } + + def "PBS should populate targeting with bidder in camel case when bidder with camel case was requested"() { + given: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.genericCamelCase = new Generic() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${GENERIC_CAMEL_CASE}"] + assert targeting["hb_size_${GENERIC_CAMEL_CASE}"] + assert targeting["hb_bidder"] == GENERIC_CAMEL_CASE.value + assert targeting["hb_bidder_${GENERIC_CAMEL_CASE}"] == GENERIC_CAMEL_CASE.value + + and: "Bid response should contain seat" + assert response.seatbid.seat == [GENERIC_CAMEL_CASE] + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(GENERIC_CAMEL_CASE.value) + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.GENERIC] + } + + def "PBS should make call for alias in upper case when soft alias specified with same name in upper case strategy"() { + given: "Default bid request with soft alias and targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.aliases = [(ALIAS.value): BidderName.GENERIC] + imp[0].ext.prebid.bidder.aliasUpperCase = new Generic() + imp[0].ext.prebid.bidder.generic = null + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.GENERIC] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [ALIAS_UPPER_CASE] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${ALIAS_UPPER_CASE}"] + assert targeting["hb_size_${ALIAS_UPPER_CASE}"] + assert targeting["hb_bidder"] == ALIAS_UPPER_CASE.value + assert targeting["hb_bidder_${ALIAS_UPPER_CASE}"] == ALIAS_UPPER_CASE.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS_UPPER_CASE.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + } + + def "PBS should populate adapter code with requested bidder when conflict with soft and hard alias"() { + given: "PBS config with bidder" + def pbsConfig = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.amx.aliases.alias.enabled" : "true", + "adapters.amx.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with amx bidder and targeting" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + imp[0].ext.prebid.bidder.generic = null + it.ext.prebid.aliases = [(ALIAS.value): BidderName.GENERIC] + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [BidderName.ALIAS] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${BidderName.ALIAS}"] + assert targeting["hb_size_${BidderName.ALIAS}"] + assert targeting["hb_bidder"] == BidderName.ALIAS.value + assert targeting["hb_bidder_${BidderName.ALIAS}"] == BidderName.ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should populate adapter code with requested bidder when conflict with soft and generic hard alias"() { + given: "PBS config with bidders" + def pbsConfig = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint" : "$networkServiceContainer.rootUri/auction".toString(), + "adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with amx bidder and targeting" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.alias = new Generic() + imp[0].ext.prebid.bidder.amx = null + imp[0].ext.prebid.bidder.generic = null + it.ext.prebid.aliases = [(ALIAS.value): AMX] + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [BidderName.ALIAS] + + and: "Response should contain seat bid" + assert response.seatbid.seat == [BidderName.ALIAS] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${BidderName.ALIAS}"] + assert targeting["hb_size_${BidderName.ALIAS}"] + assert targeting["hb_bidder"] == BidderName.ALIAS.value + assert targeting["hb_bidder_${BidderName.ALIAS}"] == BidderName.ALIAS.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(ALIAS.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should properly populate bidder code when soft alias ignore standalone adapter"() { + given: "PBS config with amx bidder" + def pbsConfig = ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with soft alias and targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.amx = new Amx() + imp[0].ext.prebid.bidder.generic = null + ext.prebid.targeting = new Targeting() + ext.prebid.aliases = [(AMX.value): BidderName.GENERIC] + } + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seat" + assert response.seatbid.seat == [AMX] + + and: "Response should contain bidder targeting" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb_${AMX}"] + assert targeting["hb_size_${AMX}"] + assert targeting["hb_bidder"] == AMX.value + assert targeting["hb_bidder_${AMX}"] == AMX.value + + and: "Response should contain repose millis with corresponding bidder" + assert response.ext.responsetimemillis.containsKey(AMX.value) + + and: "Bidder request should be valid" + assert bidder.getBidderRequests(bidRequest.id) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should merge stored imp with appnexus bidder requested when reserve field specified"() { + given: "Pbs default config with appnexus" + def pbsConfig = ["adapters.${APPNEXUS.value}.enabled" : "true", + "adapters.${APPNEXUS.value}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default stored request with specified stored imps and request" + def storedRequestId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.appNexus = AppNexus.getDefault().tap { + reserve = PBSUtils.getRandomDecimal() as Double + } + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.defaultImpression + } + storedImpDao.save(storedImp) + + and: "Save stored request with source.tid and cur" + def storedBidRequest = new BidRequest(cur: [USD], source: new Source(tid: PBSUtils.randomString)) + def storedRequest = StoredRequest.getStoredRequest(storedRequestId, storedBidRequest) + storedRequestDao.save(storedRequest) + + and: "Default basic bid with bid.ext" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, APPNEXUS).tap { + seatbid[0].bid[0].ext = new BidExt() + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should contain appnexus and generic bidder" + assert response.seatbid.size() == 2 + assert response.seatbid.seat.sort() == [APPNEXUS, BidderName.GENERIC].sort() + + and: "Bidder requests should perform two bidder call" + def bidderRequests = bidder.getBidderRequests(bidRequest.id) + assert bidderRequests.size() == 2 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } } 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..ce17a673424 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy @@ -1,50 +1,57 @@ package org.prebid.server.functional.tests +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountCacheConfig +import org.prebid.server.functional.model.config.AccountConfig +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 import org.prebid.server.functional.model.request.auction.Targeting -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.BidResponse import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.response.auction.ErrorType.CACHE +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.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 CacheSpec extends BaseSpec { - 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") + private static final String PBS_API_HEADER = 'x-pbc-api-key' + private static final Integer MAX_DATACENTER_REGION_LENGTH = 4 + private static final Integer DEFAULT_UUID_LENGTH = 36 - and: "Default VtrackRequest" - def accountId = PBSUtils.randomNumber.toString() - def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) - def request = VtrackRequest.getDefaultVtrackRequest(creative) + private static final String ACCOUNT_JSON_CREATIVE_SIZE_METRIC = "account.%s.prebid_cache.creative_size.json" + private static final String ACCOUNT_XML_CREATIVE_SIZE_METRIC = "account.%s.prebid_cache.creative_size.xml" + private static final String ACCOUNT_XML_CREATIVE_TTL_METRIC = "account.%s.prebid_cache.creative_ttl.xml" + private static final String ACCOUNT_JSON_CREATIVE_TTL_METRIC = "account.%s.prebid_cache.creative_ttl.json" - when: "PBS processes vtrack request" - defaultPbsService.sendVtrackRequest(request, accountId) + private static final String ACCOUNT_REQUEST_OK_METRIC = "account.%s.prebid_cache.requests.ok" + private static final String REQUEST_OK_METRIC = "prebid_cache.requests.ok" - then: "prebid_cache.creative_size.xml metric should be updated" - def metrics = defaultPbsService.sendCollectedMetricsRequest() - def creativeSize = creative.bytes.length - assert metrics["prebid_cache.creative_size.xml"] == creativeSize - assert metrics["prebid_cache.requests.ok"] == initialValue + 1 + private static final String JSON_CREATIVE_SIZE_GLOBAL_METRIC = "prebid_cache.creative_size.json" + private static final String XML_CREATIVE_SIZE_GLOBAL_METRIC = "prebid_cache.creative_size.xml" + private static final String XML_CREATIVE_TTL_METRIC = "prebid_cache.creative_ttl.xml" + private static final String JSON_CREATIVE_TTL_METRIC = "prebid_cache.creative_ttl.json" - and: "account..prebid_cache.creative_size.xml should be updated" - assert metrics["account.${accountId}.prebid_cache.creative_size.xml" as String] == creativeSize - assert metrics["account.${accountId}.prebid_cache.requests.ok" as String] == 1 - } + private static final String CACHE_PATH = "/${PBSUtils.randomString}".toString() + private static final String CACHE_HOST = "${PBSUtils.randomString}:${PBSUtils.getRandomNumber(0, 65535)}".toString() + private static final String INTERNAL_CACHE_PATH = '/cache' + private static final String HTTP_SCHEME = 'http' + private static final String HTTPS_SCHEME = 'https' def "PBS should update prebid_cache.creative_size.json metric when json creative is received"() { given: "Current value of metric prebid_cache.requests.ok" - def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok") + def initialValue = getCurrentMetricValue(defaultPbsService, REQUEST_OK_METRIC) and: "Default BidRequest with cache, targeting" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.enableCache() + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } and: "Default basic bid with banner creative" def asset = new Asset(id: PBSUtils.randomNumber) @@ -58,38 +65,321 @@ class CacheSpec extends BaseSpec { when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) - and: "PBS processes collected metrics request" + then: "prebid_cache.creative_size.json should be update" + def adm = bidResponse.seatbid[0].bid[0].getAdm() + def creativeSize = adm.bytes.length + + and: "prebid_cache.creative_size.json metric should be updated" def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[REQUEST_OK_METRIC] == initialValue + 1 + assert metrics[JSON_CREATIVE_SIZE_GLOBAL_METRIC] == creativeSize + + and: "account..prebid_cache.creative_size.json should be update" + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + assert metrics[ACCOUNT_JSON_CREATIVE_SIZE_METRIC.formatted(bidRequest.accountId)] == creativeSize + } + + def "PBS should update prebid_cache.creative_size.xml metric when video bid and xml creative is received"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, REQUEST_OK_METRIC) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultVideoRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + and: "Default basic bid with banner creative" + def asset = new Asset(id: PBSUtils.randomNumber) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].adm = new Adm(assets: [asset]) + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) then: "prebid_cache.creative_size.json should be update" def adm = bidResponse.seatbid[0].bid[0].getAdm() def creativeSize = adm.bytes.length - assert metrics["prebid_cache.creative_size.json"] == creativeSize - assert metrics["prebid_cache.requests.ok"] == initialValue + 1 + + and: "prebid_cache.creative_size.json metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[REQUEST_OK_METRIC] == initialValue + 1 + assert metrics[XML_CREATIVE_SIZE_GLOBAL_METRIC] == creativeSize and: "account..prebid_cache.creative_size.json should be update" - def accountId = bidRequest.site.publisher.id - assert metrics["account.${accountId}.prebid_cache.requests.ok" as String] == 1 + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + assert metrics[ACCOUNT_XML_CREATIVE_SIZE_METRIC.formatted(bidRequest.accountId)] == creativeSize } def "PBS should cache bids when targeting is specified"() { given: "Default BidRequest with cache, targeting" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.enableCache() - bidRequest.ext.prebid.targeting = new Targeting() + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) 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 pbsConfig = ['pbc.api.key': apiKey, 'cache.api-key-secured': 'false'] + def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false']) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + 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] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + 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 pbsConfig = ['pbc.api.key': apiKey, 'cache.api-key-secured': 'true'] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + 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] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should cache banner bids with cache key that include account and datacenter short name when append-trace-info-to-cache-id enabled"() { + given: "Pbs config with append-trace-info-to-cache-id" + def serverDataCenter = PBSUtils.randomString + def bannerHostTtl = PBSUtils.getRandomNumber(300, 1500) + def pbsConfig = ['cache.default-ttl-seconds.banner' : bannerHostTtl.toString(), + 'datacenter-region' : serverDataCenter, + 'cache.append-trace-info-to-cache-id': 'true' + ] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS cache key should start with account and datacenter short name" + def cacheKey = prebidCache.getRecordedRequests(bidRequest.imp.id.first).puts.flatten().first.key + assert cacheKey.startsWith("${bidRequest.accountId}-${serverDataCenter.take(MAX_DATACENTER_REGION_LENGTH)}") + + and: "PBS cache key should have length equal to default UUID" + assert cacheKey.length() == DEFAULT_UUID_LENGTH + + and: "PBS should include metrics for request" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_JSON_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == bannerHostTtl + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should cache video bids with cache key that include account and datacenter short name when append-trace-info-to-cache-id enabled"() { + given: "Pbs config with append-trace-info-to-cache-id" + def serverDataCenter = PBSUtils.randomString + def videoHostTtl = PBSUtils.getRandomNumber(300, 1500) + def pbsConfig = ['cache.default-ttl-seconds.video' : videoHostTtl.toString(), + 'datacenter-region' : serverDataCenter, + 'cache.append-trace-info-to-cache-id': 'true' + ] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultVideoRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS cache key should start with account and datacenter short name" + def cacheKey = prebidCache.getRecordedRequests(bidRequest.imp.id.first).puts.flatten().first.key + assert cacheKey.startsWith("${bidRequest.accountId}-${serverDataCenter.take(MAX_DATACENTER_REGION_LENGTH)}") + + and: "PBS cache key should have length equal to default UUID" + assert cacheKey.length() == DEFAULT_UUID_LENGTH + + and: "PBS should include metrics for account" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_JSON_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == videoHostTtl + assert metrics[ACCOUNT_XML_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == videoHostTtl + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + and: "PBS should include metrics prebid_cache_creative.{xml,json}.creative.ttl for general" + assert metrics[JSON_CREATIVE_TTL_METRIC] == videoHostTtl + assert metrics[XML_CREATIVE_TTL_METRIC] == videoHostTtl + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should cache bids with cache key that include account when append-trace-info-to-cache-id enabled and datacenter is null"() { + given: "Pbs config with append-trace-info-to-cache-id" + def bannerHostTtl = PBSUtils.getRandomNumber(300, 1500) + def pbsConfig = ['cache.default-ttl-seconds.banner' : bannerHostTtl.toString(), + 'datacenter-region' : null, + 'cache.append-trace-info-to-cache-id': 'true' + ] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS cache key should start with account and datacenter short name" + def cacheKey = prebidCache.getRecordedRequests(bidRequest.imp.id.first).puts.flatten().first.key + assert cacheKey.startsWith("${bidRequest.accountId}-") + + and: "PBS cache key should have length equal to default UUID" + assert cacheKey.length() == DEFAULT_UUID_LENGTH + + and: "PBS should include metrics for request" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_JSON_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == bannerHostTtl + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should cache bids without cache key when account ID is too large"() { + given: "Pbs config with append-trace-info-to-cache-id" + def serverDataCenter = PBSUtils.randomString + def bannerHostTtl = PBSUtils.getRandomNumber(300, 1500) + def pbsConfig = ['cache.default-ttl-seconds.banner' : bannerHostTtl.toString(), + 'datacenter-region' : serverDataCenter, + 'cache.append-trace-info-to-cache-id': 'true' + ] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting and large account ID" + def accountOverflowLength = DEFAULT_UUID_LENGTH - MAX_DATACENTER_REGION_LENGTH - 2 + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + it.setAccountId(PBSUtils.getRandomString(accountOverflowLength)) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS shouldn't contain cache key" + assert !prebidCache.getRecordedRequests(bidRequest.imp.id.first).puts.flatten().first.key + + and: "PBS should include metrics for request" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_JSON_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == bannerHostTtl + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should cache bids without cache key when append-trace-info-to-cache-id disabled"() { + given: "Pbs config with append-trace-info-to-cache-id" + def bannerHostTtl = PBSUtils.getRandomNumber(300, 1500) + def serverDataCenter = PBSUtils.randomString + def pbsConfig = ['cache.default-ttl-seconds.banner' : bannerHostTtl.toString(), + 'datacenter-region' : serverDataCenter, + 'cache.append-trace-info-to-cache-id': 'false' + ] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = new Targeting() + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS shouldn't contain cache key" + assert !prebidCache.getRecordedRequests(bidRequest.imp.id.first).puts.flatten().first.key + + and: "PBS should include metrics for request" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_JSON_CREATIVE_TTL_METRIC.formatted(bidRequest.accountId)] == bannerHostTtl + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should not cache bids when targeting isn't specified"() { given: "Default BidRequest with cache" - def bidRequest = BidRequest.defaultBidRequest - bidRequest.enableCache() - bidRequest.ext.prebid.targeting = null + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + it.ext.prebid.targeting = null + } when: "PBS processes auction request" defaultPbsService.sendAuctionRequest(bidRequest) @@ -101,8 +391,8 @@ class CacheSpec extends BaseSpec { def "PBS shouldn't response with seatbid.bid.adm in response when ext.prebid.cache.bids.returnCreative=false"() { given: "Default BidRequest with cache" def bidRequest = BidRequest.defaultBidRequest.tap { - enableCache() - ext.prebid.cache.bids.returnCreative = false + it.enableCache() + it.ext.prebid.cache.bids.returnCreative = false } and: "Default basic bid with banner creative" @@ -123,8 +413,8 @@ class CacheSpec extends BaseSpec { def "PBS should response with seatbid.bid.adm in response when ext.prebid.cache.bids.returnCreative=true"() { given: "Default BidRequest with cache" def bidRequest = BidRequest.defaultBidRequest.tap { - enableCache() - ext.prebid.cache.bids.returnCreative = true + it.enableCache() + it.ext.prebid.cache.bids.returnCreative = true } and: "Default basic bid with banner creative" @@ -145,9 +435,9 @@ class CacheSpec extends BaseSpec { def "PBS shouldn't response with seatbid.bid.adm in response when ext.prebid.cache.vastXml.returnCreative=false and video request"() { given: "Default BidRequest with cache" def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0] = Imp.getDefaultImpression(VIDEO) - enableCache() - ext.prebid.cache.vastXml.returnCreative = false + it.enableCache() + it.imp[0] = Imp.getDefaultImpression(VIDEO) + it.ext.prebid.cache.vastXml.returnCreative = false } and: "Default basic bid with banner creative" @@ -168,9 +458,9 @@ class CacheSpec extends BaseSpec { def "PBS should response with seatbid.bid.adm in response when ext.prebid.cache.vastXml.returnCreative=#returnCreative and imp.#mediaType"() { given: "Default BidRequest with cache" def bidRequest = BidRequest.defaultBidRequest.tap { - enableCache() - imp[0] = Imp.getDefaultImpression(mediaType) - ext.prebid.cache.vastXml.returnCreative = returnCreative + it.enableCache() + it.imp[0] = Imp.getDefaultImpression(mediaType) + it.ext.prebid.cache.vastXml.returnCreative = returnCreative } and: "Default basic bid with banner creative" @@ -192,4 +482,226 @@ class CacheSpec extends BaseSpec { false | BANNER true | VIDEO } + + def "PBS shouldn't cache bids when targeting is specified and config cache is invalid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.path" : CACHE_PATH, + "cache.scheme": HTTP_SCHEME, + "cache.host" : CACHE_HOST] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain error" + assert bidResponse.ext?.errors[CACHE]*.code == [999] + assert bidResponse.ext?.errors[CACHE]*.message[0] == ("Failed to resolve '${CACHE_HOST.tokenize(":")[0]}' [A(1)]") + + and: "Bid response targeting should contain value" + assert bidResponse.seatbid[0].bid[0].ext.prebid.targeting.findAll { it.key.startsWith("hb_cache") }.isEmpty() + + and: "PBS shouldn't call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 0 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG) + } + + def "PBS should cache bids and emit error when targeting is specified and config cache is valid and internal is invalid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.internal.path" : CACHE_PATH, + "cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : CACHE_HOST] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 0 + + and: "Seat bid shouldn't be discarded" + assert !bidResponse.seatbid.isEmpty() + + and: "Bid response targeting should contain value" + assert bidResponse.seatbid[0].bid[0].ext.prebid.targeting.findAll { it.key.startsWith("hb_cache") }.isEmpty() + + and: "Debug should contain http call with empty response body" + def cacheCall = bidResponse.ext.debug.httpcalls['cache'][0] + assert cacheCall.responseBody == null + assert cacheCall.uri == "${HTTP_SCHEME}://${networkServiceContainer.hostAndPort + INTERNAL_CACHE_PATH}" + + then: "Response should contain error" + assert bidResponse.ext?.errors[CACHE]*.code == [999] + assert bidResponse.ext?.errors[CACHE]*.message[0] == ("Failed to resolve '${CACHE_HOST.tokenize(":")[0]}' [A(1)]") + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG) + } + + def "PBS should cache bids when targeting is specified and config cache is invalid and internal cache config valid"() { + given: "Pbs config with cache" + def INVALID_PREBID_CACHE_CONFIG = ["cache.path" : CACHE_PATH, + "cache.scheme": HTTPS_SCHEME, + "cache.host" : CACHE_HOST,] + def VALID_INTERNAL_CACHE_CONFIG = ["cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : "$networkServiceContainer.hostAndPort".toString(), + "cache.internal.path" : INTERNAL_CACHE_PATH,] + def pbsService = pbsServiceFactory.getService(INVALID_PREBID_CACHE_CONFIG + VALID_INTERNAL_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "Bid response targeting should contain value" + verifyAll(bidResponse?.seatbid[0]?.bid[0]?.ext?.prebid?.targeting as Map) { + it.get("hb_cache_id") + it.get("hb_cache_id_generic") + it.get("hb_cache_path") == CACHE_PATH + it.get("hb_cache_host") == CACHE_HOST + it.get("hb_cache_path_generic") == CACHE_PATH + it.get("hb_cache_host_generic") == CACHE_HOST + } + + and: "Debug should contain http call" + assert bidResponse.ext.debug.httpcalls['cache'][0].uri == + "${HTTPS_SCHEME}://${CACHE_HOST + CACHE_PATH}" + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(INVALID_PREBID_CACHE_CONFIG + VALID_INTERNAL_CACHE_CONFIG) + } + + def "PBS should cache bids when targeting is specified and config cache and internal cache config valid"() { + given: "Pbs config with cache" + def VALID_INTERNAL_CACHE_CONFIG = ["cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : "$networkServiceContainer.hostAndPort".toString(), + "cache.internal.path" : INTERNAL_CACHE_PATH] + def pbsService = pbsServiceFactory.getService(VALID_INTERNAL_CACHE_CONFIG) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.defaultBidRequest.tap { + it.enableCache() + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "Bid response targeting should contain value" + verifyAll(bidResponse.seatbid[0].bid[0].ext.prebid.targeting) { + it.get("hb_cache_id") + it.get("hb_cache_id_generic") + it.get("hb_cache_path") == INTERNAL_CACHE_PATH + it.get("hb_cache_host") == networkServiceContainer.hostAndPort.toString() + it.get("hb_cache_path_generic") == INTERNAL_CACHE_PATH + it.get("hb_cache_host_generic") == networkServiceContainer.hostAndPort.toString() + } + + and: "Debug should contain http call" + assert bidResponse.ext.debug.httpcalls['cache'][0].uri == + "${HTTP_SCHEME}://${networkServiceContainer.hostAndPort + INTERNAL_CACHE_PATH}" + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(VALID_INTERNAL_CACHE_CONFIG) + } + + def "PBS should cache bids and add targeting values when account cache config #accountAuctionConfig"() { + given: "Current value of metric prebid_cache.requests.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, REQUEST_OK_METRIC) + + and: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.enableCache() + } + + 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) + + and: "Default bid response" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should call PBC" + assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1 + + and: "PBS response targeting contains bidder specific keys" + def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting + assert targetingKeyMap.containsKey('hb_cache_id') + assert targetingKeyMap.containsKey("hb_cache_id_${GENERIC}".toString()) + assert targetingKeyMap.containsKey('hb_uuid') + assert targetingKeyMap.containsKey("hb_uuid_${GENERIC}".toString()) + + and: "Metrics should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[REQUEST_OK_METRIC] == initialValue + 1 + assert metrics[ACCOUNT_REQUEST_OK_METRIC.formatted(bidRequest.accountId)] == 1 + + where: + accountAuctionConfig << [ + new AccountAuctionConfig(), + new AccountAuctionConfig(cache: new AccountCacheConfig()), + new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: null)), + new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: true)) + ] + } + + def "PBS shouldn't cache bids and add targeting values when account cache config disabled"() { + given: "Default BidRequest with cache, targeting" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.enableCache() + } + + and: "Account with cache config" + def accountAuctionConfig = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: false)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Default bid response" + def presetBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, presetBidResponse) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't call PBC" + assert !prebidCache.getRequestCount(bidRequest.imp[0].id) + + and: "PBS response targeting shouldn't contains bidder specific keys" + def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting + assert !targetingKeyMap.containsKey('hb_cache_id') + assert !targetingKeyMap.containsKey("hb_cache_id_${GENERIC}".toString()) + assert !targetingKeyMap.containsKey('hb_uuid') + assert !targetingKeyMap.containsKey("hb_uuid_${GENERIC}".toString()) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy new file mode 100644 index 00000000000..e5637fe80e2 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/CacheVtrackSpec.groovy @@ -0,0 +1,511 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountCacheConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountEventsConfig +import org.prebid.server.functional.model.config.AccountVtrackConfig +import org.prebid.server.functional.model.db.Account +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.vtrack.TransferValue +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class CacheVtrackSpec extends BaseSpec { + + private static final String ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC = "account.%s.prebid_cache.vtrack.creative_size.xml" + private static final String ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC = "account.%s.prebid_cache.vtrack.creative_ttl.xml" + private static final String ACCOUNT_VTRACK_WRITE_ERR_METRIC = "account.%s.prebid_cache.vtrack.write.err" + private static final String ACCOUNT_VTRACK_WRITE_OK_METRIC = "account.%s.prebid_cache.vtrack.write.ok" + + private static final String VTRACK_XML_CREATIVE_SIZE_METRIC = "prebid_cache.vtrack.creative_size.xml" + private static final String VTRACK_XML_CREATIVE_TTL_METRIC = "prebid_cache.vtrack.creative_ttl.xml" + private static final String VTRACK_WRITE_OK_METRIC = "prebid_cache.vtrack.write.ok" + private static final String VTRACK_WRITE_ERROR_METRIC = "prebid_cache.vtrack.write.err" + private static final String VTRACK_READ_OK_METRIC = "prebid_cache.vtrack.read.ok" + private static final String VTRACK_READ_ERROR_METRIC = "prebid_cache.vtrack.read.err" + + private static final String CACHE_ENDPOINT = "/cache" + private static final String CACHE_PATH = "/${PBSUtils.randomString}".toString() + private static final String CACHE_HOST = "${PBSUtils.randomString}:${PBSUtils.getRandomNumber(0, 65535)}".toString() + private static final String HTTP_SCHEME = 'http' + + private static final Map INVALID_PREBID_CACHE_CONFIG = ["cache.path" : CACHE_PATH, + "cache.scheme": HTTP_SCHEME, + "cache.host" : CACHE_HOST] + private static final Map VALID_INTERNAL_CACHE = ["cache.internal.scheme": HTTP_SCHEME, + "cache.internal.host" : "$networkServiceContainer.hostAndPort".toString(), + "cache.internal.path" : CACHE_ENDPOINT] + private static PrebidServerService pbsServiceWithInternalCache + + def setupSpec() { + pbsServiceWithInternalCache = pbsServiceFactory.getService(VALID_INTERNAL_CACHE + INVALID_PREBID_CACHE_CONFIG) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(VALID_INTERNAL_CACHE + INVALID_PREBID_CACHE_CONFIG) + } + + void cleanup() { + prebidCache.reset() + } + + 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.vtrack.write.ok" + def initialOkVTrackValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + 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: "Set up prebid cache" + prebidCache.setResponse() + + 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) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(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() + def ttlSeconds = request.puts[0].ttlseconds + assert metrics[VTRACK_WRITE_OK_METRIC] == initialOkVTrackValue + 1 + assert metrics[VTRACK_XML_CREATIVE_TTL_METRIC] == ttlSeconds + + and: "account..prebid_cache.vtrack.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId) as String] == 1 + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId) as String] == ttlSeconds + + 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 " + } + + 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, VTRACK_WRITE_OK_METRIC) + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "prebid_cache.vtrack.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def creativeSize = creative.bytes.length + assert metrics[VTRACK_WRITE_OK_METRIC] == initialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 + assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize + } + + def "PBS should failed VTrack request when sending request without account"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, null) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Account 'a' is required query parameter and can't be empty" + } + + def "PBS shouldn't use negative value in tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + == [requestedTtl, accountTtl].findAll { it -> it > 0 }.min() + where: + requestedTtl | accountTtl + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer | PBSUtils.getRandomNegativeNumber(-1500, 300) as Integer + } + + def "PBS should use lowest tllSecond when account vtrack ttl is #accountTtl and request ttl second is #requestedTtl"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = requestedTtl + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: accountTtl) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs should emit creative_ttl.xml with lowest value" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] == [requestedTtl, accountTtl].min() + + where: + requestedTtl | accountTtl + null | null + null | PBSUtils.getRandomNumber(300, 1500) as Integer + PBSUtils.getRandomNumber(300, 1500) as Integer | null + PBSUtils.getRandomNumber(300, 1500) as Integer | PBSUtils.getRandomNumber(300, 1500) as Integer + } + + def "PBS should proceed request when account ttl and request ttl second are empty"() { + given: "Default VtrackRequest" + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative).tap { + puts[0].ttlseconds = null + } + + and: "Cache set up response" + prebidCache.setResponse() + + and: "Create and save vtrack in account" + def accountId = PBSUtils.randomNumber.toString() + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.vtrack = new AccountVtrackConfig(ttl: null) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "Pbs shouldn't emit creative_ttl.xml" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert !metrics[ACCOUNT_VTRACK_CREATIVE_TTL_XML_METRIC.formatted(accountId)] + } + + def "PBS should return 400 status code when get vtrack request without uuid"() { + when: "PBS processes get vtrack request" + defaultPbsService.sendGetVtrackRequest(["uuid": null]) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "'uuid' is a required query parameter and can't be empty" + } + + def "PBS should return 200 status code when get vtrack request contain uuid"() { + given: "Clean up and set up successful response" + def responseBody = TransferValue.getTransferValue() + prebidCache.setGetResponse(responseBody) + + when: "PBS processes get vtrack request" + def response = defaultPbsService.sendGetVtrackRequest(["uuid": UUID.randomUUID().toString()]) + + then: "Response should contain response from pbc" + assert response == responseBody + + then: "Metrics should contain ok metric" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_OK_METRIC] == 1 + } + + def "PBS should return status code that came from pbc when get vtrack request and response from pbc invalid"() { + given: "Random uuid" + def uuid = UUID.randomUUID().toString() + + and: "Cache set up invalid response" + def randomErrorMessage = PBSUtils.randomString + prebidCache.setInvalidGetResponse(uuid, randomErrorMessage) + + when: "PBS processes get vtrack request" + defaultPbsService.sendGetVtrackRequest(["uuid": uuid]) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == INTERNAL_SERVER_ERROR.code() + assert exception.responseBody == "Error occurred while sending request to cache: Cannot parse response: $randomErrorMessage" + + and: "Metrics should contain error metric" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_ERROR_METRIC] == 1 + } + + def "PBS should return 200 status code and body when get vtrack request with uuid and ch"() { + given: "Current value of metric prebid_cache.vtrack.read.ok" + def initialValue = getCurrentMetricValue(defaultPbsService, VTRACK_READ_OK_METRIC) + + and: "Random uuid and cache host" + def uuid = UUID.randomUUID().toString() + def cacheHost = PBSUtils.randomString + + and: "Set up response body" + def responseBody = TransferValue.getTransferValue() + prebidCache.setGetResponse(responseBody) + + when: "PBS processes get vtrack request" + def response = defaultPbsService.sendGetVtrackRequest(["uuid": uuid, "ch": cacheHost]) + + then: "Response should contain response from pbc" + assert response == responseBody + + and: "Metrics should contain ok metrics" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_OK_METRIC] == initialValue + 1 + } + + def "PBS should return 200 status code and body when internal cache configured and get vtrack request with uuid and ch"() { + given: "Current value of metric prebid_cache.vtrack.read.ok" + def initialValue = getCurrentMetricValue(pbsServiceWithInternalCache, VTRACK_READ_OK_METRIC) + + and: "Flush metric" + flushMetrics(pbsServiceWithInternalCache) + + and: "Random uuid and cache host" + def uuid = UUID.randomUUID().toString() + def cacheHost = PBSUtils.randomString + + and: "Mock set up successful response" + def responseBody = TransferValue.getTransferValue() + prebidCache.setGetResponse(responseBody) + + when: "PBS processes get vtrack request" + def response = pbsServiceWithInternalCache.sendGetVtrackRequest(["uuid": uuid, "ch": cacheHost]) + + then: "Response should contain response from pbc" + assert response == responseBody + + and: "Metrics should contain ok metrics" + def metricsRequest = pbsServiceWithInternalCache.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_OK_METRIC] == initialValue + 1 + + and: "Verify parameters that came to external cache services" + def requestParams = prebidCache.getVTracGetRequestParams() + assert requestParams == "[{ch=[$cacheHost], uuid=[$uuid]}]" + } + + def "PBS should return 200 status code when internal cache and get vtrack request contain uuid"() { + given: "Current value of metric prebid_cache.vtrack.read.ok" + def initialValue = getCurrentMetricValue(pbsServiceWithInternalCache, VTRACK_READ_OK_METRIC) + + and: "Random uuid" + def uuid = UUID.randomUUID().toString() + + and: "Set up response body" + def responseBody = TransferValue.getTransferValue() + prebidCache.setGetResponse(responseBody) + + and: "Flush metric" + flushMetrics(pbsServiceWithInternalCache) + + when: "PBS processes get vtrack request" + def response = pbsServiceWithInternalCache.sendGetVtrackRequest(["uuid": uuid]) + + then: "Response should contain response from pbc" + assert response == responseBody + + and: "Metrics should contain ok metrics" + def metricsRequest = pbsServiceWithInternalCache.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_OK_METRIC] == initialValue + 1 + + and: "Verify parameters that came to external cache services" + def requestParams = prebidCache.getVTracGetRequestParams() + assert requestParams == "[{uuid=[$uuid]}]" + } + + def "PBS should return status code that came from pbc when internal cache and get vtrack request and response from pbc invalid"() { + given: "Random uuid" + def uuid = UUID.randomUUID().toString() + + and: "Cache set up invalid response" + def randomErrorMessage = PBSUtils.randomString + prebidCache.setInvalidGetResponse(uuid, randomErrorMessage) + + and: "Flush metric" + flushMetrics(pbsServiceWithInternalCache) + + when: "PBS processes get vtrack request" + pbsServiceWithInternalCache.sendGetVtrackRequest(["uuid": uuid]) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == INTERNAL_SERVER_ERROR.code() + assert exception.responseBody == "Error occurred while sending request to cache: Cannot parse response: $randomErrorMessage" + + and: "Metrics should contain error metric" + def metricsRequest = pbsServiceWithInternalCache.sendCollectedMetricsRequest() + assert metricsRequest[VTRACK_READ_ERROR_METRIC] == 1 + + and: "Verify parameters that came to external cache services" + def requestParams = prebidCache.getVTracGetRequestParams() + assert requestParams == "[{uuid=[$uuid]}]" + } + + def "PBS should return 400 status code when internal cache and get vtrack request without uuid"() { + when: "PBS processes get vtrack request" + pbsServiceWithInternalCache.sendGetVtrackRequest(["uuid": null]) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "'uuid' is a required query parameter and can't be empty" + } + + def "PBS should update prebid_cache.creative_size.xml metric when account cache config #enabledCacheConcfig"() { + given: "Current value of metric prebid_cache.requests.ok" + def okInitialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_OK_METRIC) + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Create and save enabled events config in account" + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.auction = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: enabledCacheConcfig)) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + and: "Set up prebid cache" + prebidCache.setResponse() + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "prebid_cache.creative_size.xml metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + def creativeSize = creative.bytes.length + assert metrics[VTRACK_WRITE_OK_METRIC] == okInitialValue + 1 + + and: "account..prebid_cache.creative_size.xml should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_OK_METRIC.formatted(accountId)] == 1 + assert metrics[ACCOUNT_VTRACK_XML_CREATIVE_SIZE_METRIC.formatted(accountId)] == creativeSize + + where: + enabledCacheConcfig << [null, false, true] + } + + def "PBS should failed cache and update prebid_cache.vtrack.write.err metric when cache service respond with invalid status code"() { + given: "Current value of metric prebid_cache.requests.ok" + def okInitialValue = getCurrentMetricValue(defaultPbsService, VTRACK_WRITE_ERROR_METRIC) + + and: "Default VtrackRequest" + def accountId = PBSUtils.randomNumber.toString() + def creative = encodeXml(Vast.getDefaultVastModel(PBSUtils.randomString)) + def request = VtrackRequest.getDefaultVtrackRequest(creative) + + and: "Create and save enabled events config in account" + def account = new Account().tap { + it.uuid = accountId + it.config = new AccountConfig().tap { + it.auction = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: true)) + } + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(defaultPbsService) + + and: "Reset cache and set up invalid response" + prebidCache.setInvalidPostResponse() + + when: "PBS processes vtrack request" + defaultPbsService.sendPostVtrackRequest(request, accountId) + + then: "PBS throws an exception" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 500 + assert exception.responseBody.contains("Error occurred while sending request to cache: HTTP status code 500") + + then: "prebid_cache.vtrack.write.err metric should be updated" + def metrics = defaultPbsService.sendCollectedMetricsRequest() + assert metrics[VTRACK_WRITE_ERROR_METRIC] == okInitialValue + 1 + + and: "account..prebid_cache.vtrack.write.err should be updated" + assert metrics[ACCOUNT_VTRACK_WRITE_ERR_METRIC.formatted(accountId)] == 1 + } +} 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..29542fd8326 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,16 +2281,16 @@ 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" + ["receive-cookie-deprecation=1; Max-Age=${TimeUnit.DAYS.toSeconds(7)}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned"] } def "PBS should set cookie deprecation header from the default account when default account contain privacy sandbox and request account is empty"() { @@ -2298,9 +2313,9 @@ 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" + ["receive-cookie-deprecation=1; Max-Age=${privacySandbox.cookieDeprecation.ttlSeconds}; Expires=*; Path=/; Secure; HTTPOnly; SameSite=None; Partitioned"] } def "PBS shouldn't set cookie deprecation header when cookie sync request doesn't contain account"() { @@ -2315,23 +2330,23 @@ 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) { - cookie.replaceFirst(/Expires=[^;]+;/, "Expires=*;") + private static List removeExpiresValue(List cookies) { + cookies.collect { it.replaceFirst(/Expires=[^;]+;/, "Expires=*;") } } } 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..662f9423848 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -1,30 +1,30 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.util.CurrencyUtil -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 +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY 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], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig) } - private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig) def "PBS should return currency rates"() { when: "PBS processes bidders params request" @@ -79,7 +79,7 @@ class CurrencySpec extends BaseSpec { 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 bidPrice == CurrencyUtil.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 @@ -103,7 +103,7 @@ class CurrencySpec extends BaseSpec { 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 bidPrice == CurrencyUtil.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 @@ -113,27 +113,74 @@ class CurrencySpec extends BaseSpec { JPY || USD } - private static Map getExternalCurrencyConverterConfig() { - ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, - "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"] + 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 == CurrencyUtil.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 } - private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { - return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + 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] } - private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { - def conversionRate - if (fromCurrency == toCurrency) { - conversionRate = 1 - } else if (fromCurrency in DEFAULT_CURRENCY_RATES) { - conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else { - conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + 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 } - conversionRate + + 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 } } 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..ea169ad00ee 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy @@ -3,41 +3,63 @@ package org.prebid.server.functional.tests import org.apache.commons.lang3.StringUtils import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountMetricsConfig import org.prebid.server.functional.model.db.Account 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.Site 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.ErrorType +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.BASIC +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED +import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.NONE +import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.response.auction.BidderCallType.STORED_BID_RESPONSE class DebugSpec extends BaseSpec { private static final String overrideToken = PBSUtils.randomString + private static final String ACCOUNT_METRICS_PREFIX_NAME = "account" + private static final String DEBUG_REQUESTS_METRIC = "debug_requests" + private static final String ACCOUNT_DEBUG_REQUESTS_METRIC = "account.%s.debug_requests" + private static final String REQUEST_OK_WEB_METRICS = "requests.ok.openrtb2-web" - def "PBS should return debug information when debug flag is #debug and test flag is #test"() { + def "PBS should return debug information and emit metrics when debug flag is #debug and test flag is #test"() { given: "Default BidRequest with test flag" def bidRequest = BidRequest.defaultBidRequest bidRequest.ext.prebid.debug = debug bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response should contain ext.debug" assert response.ext?.debug + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + and: "Account debug metrics shouldn't be incremented" + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + where: - debug | test - 1 | null - 1 | 0 - null | 1 + debug | test + ENABLED | null + ENABLED | DISABLED + null | ENABLED } def "PBS shouldn't return debug information when debug flag is #debug and test flag is #test"() { @@ -46,16 +68,27 @@ class DebugSpec extends BaseSpec { bidRequest.ext.prebid.debug = test bidRequest.test = test + and: "Flash metrics" + flushMetrics(defaultPbsService) + when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) then: "Response shouldn't contain ext.debug" assert !response.ext?.debug + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics should be present" + assert metricsRequest[REQUEST_OK_WEB_METRICS] == 1 + where: - debug | test - 0 | null - null | 0 + debug | test + DISABLED | null + null | DISABLED } def "PBS should not return debug information when bidder-level setting debug.allowed = false"() { @@ -64,7 +97,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -84,7 +117,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -102,10 +135,9 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED 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 +153,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"() { @@ -129,10 +165,9 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED 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 +182,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"() { @@ -155,7 +194,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -177,7 +216,7 @@ class DebugSpec extends BaseSpec { def "PBS should use default values = true for bidder-level setting debug.allow and account-level setting debug-allowed when they are not specified"() { given: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED when: "PBS processes auction request" def response = defaultPbsService.sendAuctionRequest(bidRequest) @@ -195,7 +234,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: debugAllowedAccount)) @@ -227,7 +266,7 @@ class DebugSpec extends BaseSpec { and: "Default basic generic BidRequest" def bidRequest = BidRequest.defaultBidRequest - bidRequest.ext.prebid.debug = 1 + bidRequest.ext.prebid.debug = ENABLED and: "Account in the DB" def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false)) @@ -272,11 +311,11 @@ class DebugSpec extends BaseSpec { assert response.ext?.debug where: - requestDebug || storedRequestDebug - 1 || 0 - 1 || 1 - 1 || null - null || 1 + requestDebug | storedRequestDebug + ENABLED | DISABLED + ENABLED | ENABLED + ENABLED | null + null | ENABLED } def "PBS AMP shouldn't return debug information when request flag is #requestDebug and stored request flag is #storedRequestDebug"() { @@ -301,12 +340,12 @@ class DebugSpec extends BaseSpec { assert !response.ext?.debug where: - requestDebug || storedRequestDebug - 0 || 1 - 0 || 0 - 0 || null - null || 0 - null || null + requestDebug | storedRequestDebug + DISABLED | ENABLED + DISABLED | DISABLED + DISABLED | null + null | DISABLED + null | null } def "PBS shouldn't populate call type when it's default bidder call"() { @@ -343,4 +382,157 @@ class DebugSpec extends BaseSpec { and: "Response should not contain ext.warnings" assert !response.ext?.warnings } + + def "PBS should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS shouldn't return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default basic generic bid request" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] + + and: "Request debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity detailed"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: DETAILED), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Debug metrics should be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] == 1 + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + } + + def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() { + given: "Default AMP request" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request" + def ampStoredRequest = BidRequest.defaultStoredRequest + + and: "Account in the DB" + def accountConfig = new AccountConfig( + metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel), + auction: new AccountAuctionConfig(debugAllow: true)) + def account = new Account(uuid: ampRequest.account, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = defaultPbsService.sendAmpRequest(ampRequest) + + then: "Response should contain ext.debug" + assert response.ext?.debug + + and: "Account debug metrics shouldn't be incremented" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] + + and: "Debug metrics should be incremented" + assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1 + + where: + verbosityLevel << [NONE, BASIC] + } + + def "PBS shouldn't emit auction request metric when incoming request invalid"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null) + bidRequest.ext.prebid.debug = ENABLED + + and: "Flash metrics" + flushMetrics(defaultPbsService) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.responseBody.contains("request.site should include at least one of request.site.id or request.site.page") + + and: "Debug metrics shouldn't be populated" + def metricsRequest = defaultPbsService.sendCollectedMetricsRequest() + assert !metricsRequest[DEBUG_REQUESTS_METRIC] + assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME) + + and: "General metrics shouldn't be present" + assert !metricsRequest[REQUEST_OK_WEB_METRICS] + } } 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..6bc172f3fa4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/EidsSpec.groovy @@ -0,0 +1,596 @@ +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 io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +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.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.request.auction.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED +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 = "" + private static final String RANDOM_SOURCE_ID = PBSUtils.randomString + + 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 eids shouldn't include warning for unknown bidder when test and debug disabled"() { + given: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = requestUser + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [new EidPermission(source: RANDOM_SOURCE_ID, bidders: [UNKNOWN])]) + it.ext.prebid.debug = DISABLED + it.test = DISABLED + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + + and: "Bid response shouldn't contain warning" + assert !bidResponse.ext.warnings + + where: + requestUser << [new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)]), + new User(ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])), + new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)], + ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)]))] + } + + def "PBS eids should include warning for unknown bidder when request in debug mode"() { + given: "Default bid request with generic bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + user = requestUser + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [new EidPermission(source: RANDOM_SOURCE_ID, bidders: [UNKNOWN])]) + it.ext.prebid.debug = debug + it.test = test + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + + and: "Bid response should contain warning" + assert bidResponse.ext.warnings[PREBID]?.code == [999] + assert bidResponse.ext.warnings[PREBID]?.message == + ["request.ext.prebid.data.eidPermissions[].bidders[] unrecognized biddercode: '$UNKNOWN'"] + + where: + debug | test | requestUser + DISABLED | ENABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)]) + DISABLED | ENABLED | new User(ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + DISABLED | ENABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)], ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + + ENABLED | DISABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)]) + ENABLED | DISABLED | new User(ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + ENABLED | DISABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)], ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + + ENABLED | ENABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)]) + ENABLED | ENABLED | new User(ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + ENABLED | ENABLED | new User(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)], ext: new UserExt(eids: [Eid.getDefaultEid(RANDOM_SOURCE_ID)])) + } + + 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, bidders: null)]) + } + + 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 + } + + def "PBS should pass user.eids to all bidders when any of required eid permissions doesn't match"() { + given: "Default BidRequest with eids" + def eidPermission = EidPermission.getDefaultEidPermission([RUBICON]) + def userEid = updateEidClosure(Eid.from(eidPermission)) + + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [eidPermission]) + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == [userEid] + + and: "Bid response shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + updateEidClosure << [ + { Eid eid -> eid.tap { it.inserter = null } }, + { Eid eid -> eid.tap { it.matcher = null } }, + { Eid eid -> eid.tap { it.matchMethod = null } }, + + { Eid eid -> eid.tap { it.inserter = "" } }, + { Eid eid -> eid.tap { it.matcher = "" } }, + + { Eid eid -> eid.tap { it.source = PBSUtils.randomString } }, + { Eid eid -> eid.tap { it.inserter = PBSUtils.randomString } }, + { Eid eid -> eid.tap { it.matcher = PBSUtils.randomString } }, + { Eid eid -> eid.tap { it.matchMethod = PBSUtils.randomNumber } } + ] + } + + def "PBS shouldn't pass user.eids to unmatched bidders when eidPermissions fields match user.eids"() { + given: "Default BidRequest with eids" + def eidPermissionWithUnmatchedBidder = eidPermission.tap { + bidders = [RUBICON] + } + def userEid = updateEidClosure(eidPermissionWithUnmatchedBidder) + + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [eidPermissionWithUnmatchedBidder]) + } + + when: "PBS processes auction request" + 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 shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + eidPermission | updateEidClosure + new EidPermission(source: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid) } + new EidPermission(source: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.source = eid.source } } + new EidPermission(inserter: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(inserter: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.inserter = eid.inserter } } + new EidPermission(matcher: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(matcher: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.matcher = eid.matcher } } + new EidPermission(matchMethod: PBSUtils.randomNumber) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(matchMethod: PBSUtils.randomNumber) | { EidPermission eid -> Eid.getDefaultEid().tap { it.matchMethod = eid.matchMethod } } + } + + def "PBS should filter only unauthorized eids when multiple eids with different permissions are present"() { + given: "Default BidRequest with eids" + def eidPermissionWithUnmatchedBidder = EidPermission.getDefaultEidPermission([RUBICON]) + def userEid = Eid.from(eidPermissionWithUnmatchedBidder) + def properEids = [Eid.defaultEid, Eid.defaultEid] + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: properEids + userEid) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [eidPermissionWithUnmatchedBidder]) + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == properEids + + and: "Bid response shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + } + + def "PBS should pass user.eids to matched bidders when eidPermissions fields match user.eids"() { + given: "Default BidRequest with eids" + def eidPermissionAllowingCurrentBidder = eidPermission.tap { + bidders = [[GENERIC, GENERIC_CAMEL_CASE].shuffled().first()] + } + def userEid = updateEidClosure(eidPermissionAllowingCurrentBidder) + + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [eidPermissionAllowingCurrentBidder]) + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == [userEid] + + and: "Bid response shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + eidPermission | updateEidClosure + new EidPermission(source: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid) } + new EidPermission(source: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.source = eid.source } } + new EidPermission(inserter: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(inserter: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.inserter = eid.inserter } } + new EidPermission(matcher: PBSUtils.randomString) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(matcher: PBSUtils.randomString) | { EidPermission eid -> Eid.getDefaultEid().tap { it.matcher = eid.matcher } } + new EidPermission(matchMethod: PBSUtils.randomNumber) | { EidPermission eid -> Eid.from(eid).tap { it.source = PBSUtils.randomString } } + new EidPermission(matchMethod: PBSUtils.randomNumber) | { EidPermission eid -> Eid.getDefaultEid().tap { it.matchMethod = eid.matchMethod } } + } + + def "PBS should apply most specific eidPermissions rule when multiple rules match"() { + given: "Default BidRequest with eids" + def userEid = Eid.getDefaultEid() + def moreSpecificEidPermission = moreSpecificPermissionClosure(userEid).tap { + it.bidders = [RUBICON] + } + def lessSpecificEidPermission = lessSpecificPermissionClosure(userEid).tap { + it.bidders = [GENERIC] + } + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [moreSpecificEidPermission, lessSpecificEidPermission].shuffled()) + } + + when: "PBS processes auction request" + 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 shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + moreSpecificPermissionClosure | lessSpecificPermissionClosure + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.source = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.inserter = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.matcher = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.matchMethod = null } }) + ({ Eid eid -> EidPermission.from(eid).tap { it.source = null } }) | ({ Eid eid -> new EidPermission(inserter: eid.inserter, matcher: eid.matcher) }) + ({ Eid eid -> new EidPermission(inserter: eid.inserter, matcher: eid.matcher) }) | ({ Eid eid -> new EidPermission(matchMethod: eid.matchMethod) }) + } + + def "PBS should allow access to bidder defined in most specific rule when multiple rules match"() { + given: "Default BidRequest with eids" + def userEid = Eid.getDefaultEid() + def moreSpecificEidPermission = moreSpecificPermissionClosure(userEid).tap { + it.bidders = [GENERIC] + } + def lessSpecificEidPermission = lessSpecificPermissionClosure(userEid).tap { + it.bidders = [RUBICON] + } + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [moreSpecificEidPermission, lessSpecificEidPermission].shuffled()) + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == [userEid] + + and: "Bid response shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + moreSpecificPermissionClosure | lessSpecificPermissionClosure + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.source = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.inserter = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.matcher = null } }) + ({ Eid eid -> EidPermission.from(eid) }) | ({ Eid eid -> EidPermission.from(eid).tap { it.matchMethod = null } }) + ({ Eid eid -> EidPermission.from(eid).tap { it.source = null } }) | ({ Eid eid -> new EidPermission(inserter: eid.inserter, matcher: eid.matcher) }) + ({ Eid eid -> new EidPermission(inserter: eid.inserter, matcher: eid.matcher) }) | ({ Eid eid -> new EidPermission(matchMethod: eid.matchMethod) }) + } + + def "PBS should apply permissions from any matching rule when specificity is equal"() { + given: "Default BidRequest with eids" + def userEid = Eid.getDefaultEid() + def allowingRule = allowingPermissionClosure(userEid).tap { + it.bidders = [GENERIC] + } + def restrictingRule = restrictingPermissionClosure(userEid).tap { + it.bidders = [RUBICON] + } + def bidRequest = BidRequest.defaultBidRequest.tap { + user = new User(eids: [userEid]) + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [allowingRule, restrictingRule].shuffled()) + } + + when: "PBS processes auction request" + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain requested eids" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids == [userEid] + + and: "Bid response shouldn't contain any errors and warnings" + assert !bidResponse.ext.errors + assert !bidResponse.ext.warnings + + where: + allowingPermissionClosure | restrictingPermissionClosure + ({ Eid eid -> new EidPermission(source: eid.source) }) | ({ Eid eid -> new EidPermission(inserter: eid.inserter) }) + ({ Eid eid -> new EidPermission(matcher: eid.matcher) }) | ({ Eid eid -> new EidPermission(source: eid.source) }) + ({ Eid eid -> new EidPermission(matchMethod: eid.matchMethod) }) | ({ Eid eid -> new EidPermission(matcher: eid.matcher) }) + ({ Eid eid -> new EidPermission(source: eid.source, matcher: eid.matcher) }) | ({ Eid eid -> new EidPermission(inserter: eid.inserter, matchMethod: eid.matchMethod) }) + ({ Eid eid -> new EidPermission(source: eid.source, inserter: eid.inserter) }) | ({ Eid eid -> new EidPermission(matcher: eid.matcher, matchMethod: eid.matchMethod) }) + ({ Eid eid -> new EidPermission(source: eid.source, matchMethod: eid.matchMethod) }) | ({ Eid eid -> new EidPermission(inserter: eid.inserter, matcher: eid.matcher) }) + ({ Eid eid -> EidPermission.from(eid).tap { matchMethod = null } }) | ({ Eid eid -> EidPermission.from(eid).tap { matcher = null } }) + } + + def "PBS should throw an error when all eidPermissions fields are empty"() { + given: "Default bid request with invalid eidPermission" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.data = new ExtRequestPrebidData(eidpermissions: [new EidPermission()]) + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should throw error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == "Invalid request format: " + + "Missing required parameter(s) in request.ext.prebid.data.eidPermissions[]. " + + "Either one or a combination of inserter, source, matcher, or mm should be defined." + } +} 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..4a0229122b6 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/HttpSettingsSpec.groovy @@ -15,7 +15,6 @@ import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.HttpSettings import org.prebid.server.functional.util.PBSUtils import org.prebid.server.util.ResourceUtil -import spock.lang.Shared import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer @@ -23,20 +22,34 @@ import static org.prebid.server.functional.testcontainers.Dependencies.networkSe class HttpSettingsSpec extends BaseSpec { // Check that PBS actually applied account config only possible by relying on side effects. - @Shared - HttpSettings httpSettings = new HttpSettings(networkServiceContainer) + static PrebidServerService prebidServerService + static PrebidServerService prebidServerServiceWithRfc - @Shared - PrebidServerService prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig) + private static final HttpSettings httpSettings = new HttpSettings(networkServiceContainer) + private static final Map PBS_CONFIG_WITH_RFC = new HashMap<>(PbsConfig.httpSettingsConfig) + + ['settings.http.endpoint': "${networkServiceContainer.rootUri}${HttpSettings.rfcEndpoint}".toString(), + 'settings.http.rfc3986-compatible': 'true'] + + def setupSpec() { + prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig) + prebidServerServiceWithRfc = pbsServiceFactory.getService(PBS_CONFIG_WITH_RFC) + bidder.setResponse() + vendorList.setResponse() + } + + def cleanupSpec() { + prebidServerService = pbsServiceFactory.removeContainer(PbsConfig.httpSettingsConfig) + prebidServerService = pbsServiceFactory.removeContainer(PBS_CONFIG_WITH_RFC) + } 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) - httpSettings.setResponse(bidRequest?.site?.publisher?.id, httpSettingsResponse) + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest.accountId) + httpSettings.setResponse(bidRequest.accountId, httpSettingsResponse) when: "PBS processes auction request" def response = prebidServerService.sendAuctionRequest(bidRequest) @@ -51,7 +64,32 @@ class HttpSettingsSpec extends BaseSpec { assert bidder.getRequestCount(bidRequest.id) == 1 and: "There should be only one account request" - assert httpSettings.getRequestCount(bidRequest?.site?.publisher?.id) == 1 + assert httpSettings.getRequestCount(bidRequest.accountId) == 1 + } + + def "PBS should take account information from http data source on auction request when rfc3986 enabled"() { + given: "Get basic BidRequest with generic bidder and set gdpr = 1" + def bidRequest = BidRequest.defaultBidRequest + bidRequest.regs.gdpr = 1 + + and: "Prepare default account response with gdpr = 0" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(bidRequest.accountId) + httpSettings.setRfcResponse(bidRequest.accountId, httpSettingsResponse) + + when: "PBS processes auction request" + def response = prebidServerServiceWithRfc.sendAuctionRequest(bidRequest) + + then: "Response should contain basic fields" + assert response.id + assert response.seatbid?.size() == 1 + assert response.seatbid.first().seat == GENERIC + assert response.seatbid?.first()?.bid?.size() == 1 + + and: "There should be only one call to bidder" + assert bidder.getRequestCount(bidRequest.id) == 1 + + and: "There should be only one account request" + assert httpSettings.getRfcRequestCount(bidRequest.accountId) == 1 } def "PBS should take account information from http data source on AMP request"() { @@ -61,7 +99,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) @@ -84,6 +122,36 @@ class HttpSettingsSpec extends BaseSpec { assert !response.ext?.debug?.httpcalls?.isEmpty() } + def "PBS should take account information from http data source on AMP request when rfc3986 enabled"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Get basic stored request and set gdpr = 1" + def ampStoredRequest = BidRequest.defaultBidRequest + ampStoredRequest.site.publisher.id = ampRequest.account + ampStoredRequest.regs.gdpr = 1 + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Prepare default account response with gdpr = 0" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(ampRequest.account.toString()) + httpSettings.setRfcResponse(ampRequest.account.toString(), httpSettingsResponse) + + when: "PBS processes amp request" + def response = prebidServerServiceWithRfc.sendAmpRequest(ampRequest) + + then: "Response should contain httpcalls" + assert !response.ext?.debug?.httpcalls?.isEmpty() + + and: "There should be only one account request" + assert httpSettings.getRfcRequestCount(ampRequest.account.toString()) == 1 + + then: "Response should contain targeting" + assert !response.ext?.debug?.httpcalls?.isEmpty() + } + def "PBS should take account information from http data source on event request"() { given: "Default EventRequest" def eventRequest = EventRequest.defaultEventRequest @@ -103,12 +171,32 @@ class HttpSettingsSpec extends BaseSpec { assert httpSettings.getRequestCount(eventRequest.accountId.toString()) == 1 } + def "PBS should take account information from http data source on event request when rfc3986 enabled"() { + given: "Default EventRequest" + def eventRequest = EventRequest.defaultEventRequest + + and: "Prepare default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(eventRequest.accountId.toString()) + httpSettings.setRfcResponse(eventRequest.accountId.toString(), httpSettingsResponse) + + when: "PBS processes event request" + def responseBody = prebidServerServiceWithRfc.sendEventRequest(eventRequest) + + then: "Event response should contain and corresponding content-type" + assert responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + and: "There should be only one account request" + assert httpSettings.getRfcRequestCount(eventRequest.accountId.toString()) == 1 + } + def "PBS should take account information from http data source on setuid request"() { given: "Pbs config with adapters.generic.usersync.redirect.*" - def prebidServerService = pbsServiceFactory.getService(PbsConfig.httpSettingsConfig + + def pbsConfig = PbsConfig.httpSettingsConfig + ["adapters.generic.usersync.redirect.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), "adapters.generic.usersync.redirect.support-cors" : "false", - "adapters.generic.usersync.redirect.format-override": "blank"]) + "adapters.generic.usersync.redirect.format-override": "blank"] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Get default SetuidRequest and set account, gdpr=1 " def request = SetuidRequest.defaultSetuidRequest @@ -123,14 +211,53 @@ class HttpSettingsSpec extends BaseSpec { when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - then: "Response should contain uids cookie" - assert !response.uidsCookie.tempUIDs + then: "Response should contain tempUIDs cookie" assert !response.uidsCookie.uids + assert response.uidsCookie.tempUIDs assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") and: "There should be only one account request" assert httpSettings.getRequestCount(request.account) == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should take account information from http data source on setuid request when rfc3986 enabled"() { + given: "Pbs config with adapters.generic.usersync.redirect.*" + def pbsConfig = new HashMap<>(PbsConfig.httpSettingsConfig) + + ['settings.http.endpoint': "${networkServiceContainer.rootUri}${HttpSettings.rfcEndpoint}".toString(), + 'settings.http.rfc3986-compatible': 'true', + 'adapters.generic.usersync.redirect.url' : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), + 'adapters.generic.usersync.redirect.support-cors' : 'false', + 'adapters.generic.usersync.redirect.format-override': 'blank'] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Get default SetuidRequest and set account, gdpr=1 " + def request = SetuidRequest.defaultSetuidRequest + request.gdpr = 1 + request.account = PBSUtils.randomNumber.toString() + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Prepare default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(request.account) + httpSettings.setRfcResponse(request.account, httpSettingsResponse) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain tempUIDs cookie" + assert !response.uidsCookie.uids + assert response.uidsCookie.tempUIDs + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + and: "There should be only one account request" + assert httpSettings.getRfcRequestCount(request.account) == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should take account information from http data source on vtrack request"() { @@ -144,7 +271,7 @@ class HttpSettingsSpec extends BaseSpec { httpSettings.setResponse(accountId, httpSettingsResponse) when: "PBS processes vtrack request" - def response = prebidServerService.sendVtrackRequest(request, accountId) + def response = prebidServerService.sendPostVtrackRequest(request, accountId) then: "Response should contain uid" assert response.responses[0]?.uuid @@ -158,6 +285,31 @@ class HttpSettingsSpec extends BaseSpec { assert prebidCacheRequest.contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") } + def "PBS should take account information from http data source on vtrack request when rfc3986 enabled"() { + given: "Default VtrackRequest" + String payload = PBSUtils.randomString + def request = VtrackRequest.getDefaultVtrackRequest(encodeXml(Vast.getDefaultVastModel(payload))) + def accountId = PBSUtils.randomNumber.toString() + + and: "Prepare default account response" + def httpSettingsResponse = HttpAccountsResponse.getDefaultHttpAccountsResponse(accountId) + httpSettings.setRfcResponse(accountId, httpSettingsResponse) + + when: "PBS processes vtrack request" + def response = prebidServerServiceWithRfc.sendPostVtrackRequest(request, accountId) + + then: "Response should contain uid" + assert response.responses[0]?.uuid + + and: "There should be only one account request and pbc request" + assert httpSettings.getRfcRequestCount(accountId.toString()) == 1 + assert prebidCache.getXmlRequestCount(payload) == 1 + + and: "VastXml that was send to PrebidCache must contain event url" + def prebidCacheRequest = prebidCache.getXmlRecordedRequestsBody(payload)[0] + assert prebidCacheRequest.contains("/event?t=imp&b=${request.puts[0].bidid}&a=$accountId&bidder=${request.puts[0].bidder}") + } + def "PBS should return error if account settings isn't found"() { given: "Default EventRequest" def eventRequest = EventRequest.defaultEventRequest @@ -170,4 +322,17 @@ class HttpSettingsSpec extends BaseSpec { assert exception.statusCode == 401 assert exception.responseBody.contains("Account '$eventRequest.accountId' doesn't support events") } + + def "PBS should return error if account settings isn't found when rfc3986 enabled"() { + given: "Default EventRequest" + def eventRequest = EventRequest.defaultEventRequest + + when: "PBS processes event request" + prebidServerServiceWithRfc.sendEventRequest(eventRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 401 + assert exception.responseBody.contains("Account '$eventRequest.accountId' doesn't support events") + } } 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..7a2f5923d56 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy @@ -0,0 +1,335 @@ +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.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.bidder.BidderName.GENER_X +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.bidder.generic = null + ext.prebid.bidder.alias = new Generic() + } + ext.prebid.aliases = [(aliasName.value): bidderName] + } + + 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 should update imp fields only for specific alias when request has multiple aliases"() { + given: "Default basic BidRequest" + def storedPmp = Pmp.defaultPmp + def originalPmp = Pmp.defaultPmp + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.tap { + pmp = originalPmp + ext.prebid.imp = [(aliasName): new Imp(pmp: storedPmp)] + ext.prebid.bidder.generic = null + ext.prebid.bidder.generX = new Generic() + ext.prebid.bidder.alias = new Generic() + } + ext.prebid.aliases = [(GENER_X.value) : bidderName, + (aliasName.value): bidderName, + ] + } + + when: "Requesting PBS auction" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should update imp information for specific alias" + def bidderRequests = getRequests(response) + assert bidderRequests.size() == 2 + assert bidderRequests[ALIAS.value].imp.pmp.flatten() == [storedPmp] + + and: "Left original information for other" + assert bidderRequests[GENER_X.value].imp.pmp.flatten() == [originalPmp] + + 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/InfluxDBSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/InfluxDBSpec.groovy new file mode 100644 index 00000000000..4e0253c1f95 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/InfluxDBSpec.groovy @@ -0,0 +1,78 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.AccountStatus +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.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService + +import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED +import static org.prebid.server.functional.testcontainers.Dependencies.influxdbContainer + +class InfluxDBSpec extends BaseSpec { + + private static final String ACCOUNT_REJECTED_METRIC = "influx.metric.account.%s.requests.rejected.invalid-account" + + private static final Map PBS_CONFIG_WITH_INFLUX_AND_ENFORCE_VALIDATION_ACCOUNTANT = [ + "metrics.influxdb.enabled" : "true", + "metrics.influxdb.prefix" : "influx.metric.", + "metrics.influxdb.host" : influxdbContainer.getNetworkAliases().get(0), + "metrics.influxdb.port" : influxdbContainer.getExposedPorts().get(0) as String, + "metrics.influxdb.protocol" : "http", + "metrics.influxdb.database" : influxdbContainer.database as String, + "metrics.influxdb.auth" : "${influxdbContainer.username}:${influxdbContainer.password}" as String, + "metrics.influxdb.interval" : "1", + "metrics.influxdb.connectTimeout": "5000", + "metrics.influxdb.readTimeout" : "100", + "settings.enforce-valid-account": true as String] + + private static final PrebidServerService pbsServiceWithEnforceValidAccount + = pbsServiceFactory.getService(PBS_CONFIG_WITH_INFLUX_AND_ENFORCE_VALIDATION_ACCOUNTANT) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG_WITH_INFLUX_AND_ENFORCE_VALIDATION_ACCOUNTANT) + } + + def "PBS should reject request with error and metrics when inactive account"() { + given: "Default basic BidRequest with inactive account id" + def bidRequest = BidRequest.defaultBidRequest + + and: "Inactive account id" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(status: AccountStatus.INACTIVE)) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithEnforceValidAccount.sendAuctionRequest(bidRequest) + + then: "PBS should reject the entire auction" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAUTHORIZED.code() + assert exception.responseBody == "Account $bidRequest.accountId is inactive" + + and: "PBS wait until get metric" + assert pbsServiceWithEnforceValidAccount.isContainMetricByValue(ACCOUNT_REJECTED_METRIC.formatted(bidRequest.accountId)) + + and: "PBS metrics populated correctly" + def influxMetricsRequest = pbsServiceWithEnforceValidAccount.sendInfluxMetricsRequest() + assert influxMetricsRequest[ACCOUNT_REJECTED_METRIC.formatted(bidRequest.accountId)] == 1 + } + + def "PBS shouldn't reject request with error and metrics when active account"() { + given: "Default basic BidRequest with inactive account id" + def bidRequest = BidRequest.defaultBidRequest + + and: "Inactive account id" + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(status: AccountStatus.ACTIVE)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithEnforceValidAccount.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seatBid" + assert response.seatbid.size() == 1 + + and: "PBs shouldn't emit metric" + assert !pbsServiceWithEnforceValidAccount.isContainMetricByValue(ACCOUNT_REJECTED_METRIC.formatted(bidRequest.accountId)) + } +} 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..ee3e808a56e 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,19 +141,19 @@ 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 and: "alert.general metric should be updated" - assert metrics["alerts.general" as String] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Other channel types should not be populated" assert !metrics["account.${accountId}.requests.type.openrtb2-web" as String] 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,12 +170,12 @@ 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 and: "alert.general metric should be updated" - assert metrics["alerts.general" as String] == 1 + assert metrics[ALERT_GENERAL] == 1 and: "Other channel types should not be populated" assert !metrics["account.${accountId}.requests.type.openrtb2-dooh" as String] 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/ProfileSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy new file mode 100644 index 00000000000..c6d600d518c --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/ProfileSpec.groovy @@ -0,0 +1,1562 @@ +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.AccountProfilesConfigs +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredProfileImp +import org.prebid.server.functional.model.db.StoredProfileRequest +import org.prebid.server.functional.model.db.StoredRequest +import org.prebid.server.functional.model.db.StoredResponse +import org.prebid.server.functional.model.filesystem.FileSystemAccountsConfig +import org.prebid.server.functional.model.request.amp.AmpRequest +import org.prebid.server.functional.model.request.auction.App +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.Format +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.ImpExtPrebid +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse +import org.prebid.server.functional.model.request.auction.StoredBidResponse +import org.prebid.server.functional.model.request.profile.Profile +import org.prebid.server.functional.model.request.profile.ImpProfile +import org.prebid.server.functional.model.request.profile.ProfileMergePrecedence +import org.prebid.server.functional.model.request.profile.RequestProfile +import org.prebid.server.functional.model.request.profile.ProfileType +import org.prebid.server.functional.model.request.auction.Site +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.repository.dao.ProfileImpDao +import org.prebid.server.functional.repository.dao.ProfileRequestDao +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.container.PrebidServerContainer +import org.prebid.server.functional.util.PBSUtils +import org.testcontainers.images.builder.Transferable +import spock.lang.PendingFeature + +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.request.profile.ProfileMergePrecedence.PROFILE +import static org.prebid.server.functional.model.request.profile.ProfileMergePrecedence.REQUEST +import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO + +class ProfileSpec extends BaseSpec { + + private static final String PROFILES_PATH = '/app/prebid-server/profiles' + private static final String REQUESTS_PATH = '/app/prebid-server/requests' + private static final String IMPS_PATH = '/app/prebid-server/imps' + private static final String RESPONSES_PATH = '/app/prebid-server/responses' + private static final String CATEGORIES_PATH = '/app/prebid-server/categories' + private static final String SETTINGS_FILENAME = '/app/prebid-server/settings.yaml' + private static final Integer LIMIT_HOST_PROFILE = 2 + private static final Integer ACCOUNT_ID_FILE_STORAGE = PBSUtils.randomNumber + + private static final Map FILESYSTEM_CONFIG = [ + 'settings.filesystem.settings-filename' : SETTINGS_FILENAME, + 'settings.filesystem.profiles-dir' : PROFILES_PATH, + 'settings.filesystem.stored-requests-dir' : REQUESTS_PATH, + 'settings.filesystem.stored-imps-dir' : IMPS_PATH, + 'settings.filesystem.stored-responses-dir': RESPONSES_PATH, + 'settings.filesystem.categories-dir' : CATEGORIES_PATH + ] + + private static final Map PROFILES_CONFIG = [ + 'auction.profiles.fail-on-unknown': "false", + 'auction.profiles.limit' : LIMIT_HOST_PROFILE.toString(), + 'settings.database.profiles-query': "SELECT accountId, profileId, profile, mergePrecedence, type FROM profiles " + + "WHERE profileId in (%REQUEST_ID_LIST%, %IMP_ID_LIST%)".toString()] + + private static final String LIMIT_ERROR_MESSAGE = 'Profiles exceeded the limit.' + private static final String CONFIG_ERROR_MESSAGE = 'Profiles storage not configured.' + private static final String INVALID_REQUEST_PREFIX = 'Invalid request format: Error during processing profiles: ' + private static final String NO_IMP_PROFILE_MESSAGE = "No imp profiles for ids [%s] were found" + private static final String NO_REQUEST_PROFILE_MESSAGE = "No request profiles for ids [%s] were found" + private static final String NO_PROFILE_MESSAGE = "No profile found for id: %s" + + private static final String LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.limit_exceeded" + private static final String MISSING_ACCOUNT_PROFILE_METRIC = "account.%s.profiles.missing" + + private static final ProfileImpDao profileImpDao = repository.profileImpDao + private static final ProfileRequestDao profileRequestDao = repository.profileRequestDao + + private static PrebidServerContainer pbsContainer + private static PrebidServerService pbsWithStoredProfiles + private static RequestProfile fileRequestProfile + private static RequestProfile fileRequestProfileWithEmptyMerge + private static ImpProfile fileImpProfile + private static ImpProfile fileImpProfileWithEmptyMerge + + def setupSpec() { + pbsContainer = new PrebidServerContainer(FILESYSTEM_CONFIG + PROFILES_CONFIG) + fileRequestProfile = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + fileImpProfile = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()) + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfile)), "$PROFILES_PATH/${fileRequestProfile.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfile)), "$PROFILES_PATH/${fileImpProfile.fileName}") + fileRequestProfileWithEmptyMerge = RequestProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + mergePrecedence = null + } + fileImpProfileWithEmptyMerge = ImpProfile.getProfile(ACCOUNT_ID_FILE_STORAGE.toString()).tap { + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } + mergePrecedence = null + } + pbsContainer.withCopyToContainer(Transferable.of(encode(fileRequestProfileWithEmptyMerge)), "$PROFILES_PATH/${fileRequestProfileWithEmptyMerge.fileName}") + pbsContainer.withCopyToContainer(Transferable.of(encode(fileImpProfileWithEmptyMerge)), "$PROFILES_PATH/${fileImpProfileWithEmptyMerge.fileName}") + pbsContainer.withFolder(REQUESTS_PATH) + pbsContainer.withFolder(IMPS_PATH) + pbsContainer.withFolder(RESPONSES_PATH) + pbsContainer.withFolder(CATEGORIES_PATH) + def accountsConfig = new FileSystemAccountsConfig(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]) + pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(accountsConfig)), + SETTINGS_FILENAME) + pbsContainer.start() + pbsWithStoredProfiles = new PrebidServerService(pbsContainer) + } + + def cleanupSpec() { + pbsContainer.stop() + } + + def "PBS should use profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + } + + def "PBS should use imp profile for request when it exist in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId) + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [impProfile.body.id] + it.banner == [impProfile.body.banner] + } + } + + def "PBS should use profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfile]) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == fileRequestProfile.body.site.id + it.site.name == fileRequestProfile.body.site.name + it.site.domain == fileRequestProfile.body.site.domain + it.site.cat == fileRequestProfile.body.site.cat + it.site.sectionCat == fileRequestProfile.body.site.sectionCat + it.site.pageCat == fileRequestProfile.body.site.pageCat + it.site.page == fileRequestProfile.body.site.page + it.site.ref == fileRequestProfile.body.site.ref + it.site.search == fileRequestProfile.body.site.search + it.site.keywords == fileRequestProfile.body.site.keywords + it.site.ext.data == fileRequestProfile.body.site.ext.data + + it.device.didsha1 == fileRequestProfile.body.device.didsha1 + it.device.didmd5 == fileRequestProfile.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfile.body.device.dpidsha1 + it.device.ifa == fileRequestProfile.body.device.ifa + it.device.macsha1 == fileRequestProfile.body.device.macsha1 + it.device.macmd5 == fileRequestProfile.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should use imp profile for request when it exist in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id).imp) { + it.id == [fileImpProfile.body.id] + it.banner == [fileImpProfile.body.banner] + } + } + + def "PBS should use request profile for amp request"() { + given: "Default AmpRequest" + def accountId = PBSUtils.randomNumber as String + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Stored request with profile" + def requestProfile = RequestProfile.getProfile(accountId) + def ampStoredRequest = getRequestWithProfiles(accountId, [requestProfile]) + ampStoredRequest.setAccountId(ampRequest.account) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsWithStoredProfiles.sendAmpRequest(ampRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(ampStoredRequest.id)) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + } + + def "PBS should use imp profile for amp request"() { + given: "Default AmpRequest" + def accountId = PBSUtils.randomNumber as String + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Stored request with profile" + def impProfile = ImpProfile.getProfile(accountId) + def ampStoredRequest = getRequestWithProfiles(accountId, [impProfile]) + ampStoredRequest.setAccountId(ampRequest.account) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Save storedRequest into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + def response = pbsWithStoredProfiles.sendAmpRequest(ampRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + verifyAll(bidder.getBidderRequest(ampStoredRequest.id).imp) { + it.id == [impProfile.body.id] + it.banner == [impProfile.body.banner] + } + } + + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in database"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId).tap { + it.mergePrecedence = null + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + it.site = Site.configFPDSite + } as BidRequest + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from original request when data is present" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + } + + and: "Bidder request should contain data from profile when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + } + + def "PBS should set merge strategy to default profile without error for request profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with request profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileRequestProfileWithEmptyMerge]).tap { + it.site = Site.configFPDSite + } as BidRequest + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from original request when data is present" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + } + + and: "Bidder request should contain data from original request when data is empty" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.device.didsha1 == fileRequestProfileWithEmptyMerge.body.device.didsha1 + it.device.didmd5 == fileRequestProfileWithEmptyMerge.body.device.didmd5 + it.device.dpidsha1 == fileRequestProfileWithEmptyMerge.body.device.dpidsha1 + it.device.ifa == fileRequestProfileWithEmptyMerge.body.device.ifa + it.device.macsha1 == fileRequestProfileWithEmptyMerge.body.device.macsha1 + it.device.macmd5 == fileRequestProfileWithEmptyMerge.body.device.macmd5 + it.device.dpidmd5 == fileRequestProfileWithEmptyMerge.body.device.dpidmd5 + } + } + + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in database"() { + given: "Default bidRequest with imp profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId).tap { + it.mergePrecedence = null + body.banner.tap { + btype = [PBSUtils.randomNumber] + format = [Format.randomFormat] + } + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == impProfile.body.banner.btype + } + + def "PBS should set merge strategy to default profile without error for imp profile when merge strategy is empty in filesystem"() { + given: "Default bidRequest with imp profile" + def bidRequest = getRequestWithProfiles(ACCOUNT_ID_FILE_STORAGE.toString(), [fileImpProfileWithEmptyMerge]) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile when data is present" + def bidderImpBanner = bidder.getBidderRequest(bidRequest.id).imp.banner.first + assert bidderImpBanner.format == bidRequest.imp.first.banner.format + + and: "Bidder request should contain data from profile when data is empty" + assert bidderImpBanner.btype == fileImpProfileWithEmptyMerge.body.banner.btype + } + + def "PBS should merge latest-specified profile when there merge conflict and different merge precedence present"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + firstProfile.accountId = accountId + secondProfile.accountId = accountId + def bidRequest = getRequestWithProfiles(accountId, [firstProfile, secondProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profiles" + def mergedRequest = [firstProfile, secondProfile].find { it.mergePrecedence == PROFILE }.body + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == mergedRequest.site.id + it.site.name == mergedRequest.site.name + it.site.domain == mergedRequest.site.domain + it.site.cat == mergedRequest.site.cat + it.site.sectionCat == mergedRequest.site.sectionCat + it.site.pageCat == mergedRequest.site.pageCat + it.site.page == mergedRequest.site.page + it.site.ref == mergedRequest.site.ref + it.site.search == mergedRequest.site.search + it.site.keywords == mergedRequest.site.keywords + it.site.ext.data == mergedRequest.site.ext.data + + it.device.didsha1 == mergedRequest.device.didsha1 + it.device.didmd5 == mergedRequest.device.didmd5 + it.device.dpidsha1 == mergedRequest.device.dpidsha1 + it.device.ifa == mergedRequest.device.ifa + it.device.macsha1 == mergedRequest.device.macsha1 + it.device.macmd5 == mergedRequest.device.macmd5 + it.device.dpidmd5 == mergedRequest.device.dpidmd5 + } + + where: + firstProfile | secondProfile + RequestProfile.getProfile().tap { mergePrecedence = REQUEST } | RequestProfile.getProfile() + RequestProfile.getProfile() | RequestProfile.getProfile().tap { mergePrecedence = REQUEST } + } + + def "PBS should merge first-specified profile with request merge precedence when there merge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + it.mergePrecedence = REQUEST + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == firstRequestProfile.body.site.id + it.site.name == firstRequestProfile.body.site.name + it.site.domain == firstRequestProfile.body.site.domain + it.site.cat == firstRequestProfile.body.site.cat + it.site.sectionCat == firstRequestProfile.body.site.sectionCat + it.site.pageCat == firstRequestProfile.body.site.pageCat + it.site.ref == firstRequestProfile.body.site.ref + it.site.search == firstRequestProfile.body.site.search + it.site.keywords == firstRequestProfile.body.site.keywords + it.site.ext.data == firstRequestProfile.body.site.ext.data + + it.device.didsha1 == firstRequestProfile.body.device.didsha1 + it.device.didmd5 == firstRequestProfile.body.device.didmd5 + it.device.dpidsha1 == firstRequestProfile.body.device.dpidsha1 + it.device.ifa == firstRequestProfile.body.device.ifa + it.device.macsha1 == firstRequestProfile.body.device.macsha1 + it.device.macmd5 == firstRequestProfile.body.device.macmd5 + it.device.dpidmd5 == firstRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should merge latest-specified profile with profile merge precedence when there merge conflict"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = Device.default + it.body.site = Site.rootFPDSite + } + def bidRequest = getRequestWithProfiles(accountId, [firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == secondRequestProfile.body.site.id + it.site.name == secondRequestProfile.body.site.name + it.site.domain == secondRequestProfile.body.site.domain + it.site.cat == secondRequestProfile.body.site.cat + it.site.sectionCat == secondRequestProfile.body.site.sectionCat + it.site.pageCat == secondRequestProfile.body.site.pageCat + it.site.page == secondRequestProfile.body.site.page + it.site.ref == secondRequestProfile.body.site.ref + it.site.search == secondRequestProfile.body.site.search + it.site.keywords == secondRequestProfile.body.site.keywords + it.site.ext.data == secondRequestProfile.body.site.ext.data + + it.device.didsha1 == secondRequestProfile.body.device.didsha1 + it.device.didmd5 == secondRequestProfile.body.device.didmd5 + it.device.dpidsha1 == secondRequestProfile.body.device.dpidsha1 + it.device.ifa == secondRequestProfile.body.device.ifa + it.device.macsha1 == secondRequestProfile.body.device.macsha1 + it.device.macmd5 == secondRequestProfile.body.device.macmd5 + it.device.dpidmd5 == secondRequestProfile.body.device.dpidmd5 + } + } + + def "PBS should prioritise profile for request and emit warning when request is overloaded by profiles"() { + given: "Default bidRequest with profiles" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.rootFPDSite + def profileDevice = Device.default + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = profileSite + it.body.device = null + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = null + it.body.device = profileDevice + } + def impProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def bidRequest = getRequestWithProfiles(accountId, [impProfile, firstRequestProfile, secondRequestProfile]) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + it.site.ext.data == profileSite.ext.data + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 + } + + and: "Bidder imp should contain original data from request" + assert verifyAll(bidderRequest.imp) { + it.banner == bidRequest.imp.banner + it.video == [null] + } + } + + def "PBS should be able override profile limit by account config and use remaining limits for each imp separately"() { + given: "BidRequest with profiles" + def accountId = PBSUtils.randomNumber as String + def profileSite = Site.defaultSite + def profileDevice = Device.default + def firstRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.device = null + it.body.site = profileSite + } + def secondRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.site = null + it.body.device = profileDevice + } + def firstImp = Imp.defaultImpression.tap { + it.banner.btype = [PBSUtils.randomNumber] + } + def secondImp = Imp.defaultImpression.tap { + it.banner.battr = [PBSUtils.randomNumber] + } + def thirdImp = Imp.defaultImpression.tap { + it.banner.mimes = [PBSUtils.randomString] + } + def firstImpProfile = ImpProfile.getProfile(accountId, firstImp) + def secondImpProfile = ImpProfile.getProfile(accountId, secondImp) + def thirdImpProfile = ImpProfile.getProfile(accountId, thirdImp) + def bidRequest = getRequestWithProfiles(accountId, [firstImpProfile, secondImpProfile, firstRequestProfile, secondRequestProfile]).tap { + imp << new Imp(ext: new ImpExt(prebid: new ImpExtPrebid(profileNames: [secondImpProfile, thirdImpProfile].id))) + } as BidRequest + + and: "Default account" + def profilesConfigs = new AccountProfilesConfigs(limit: LIMIT_HOST_PROFILE + 2) + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(firstRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(secondRequestProfile)) + profileImpDao.save(StoredProfileImp.getProfile(firstImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(secondImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(thirdImpProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Missing metric shouldn't increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert !metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] + + and: "Bidder request should contain data from profiles" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == profileSite.id + it.site.name == profileSite.name + it.site.domain == profileSite.domain + it.site.cat == profileSite.cat + it.site.sectionCat == profileSite.sectionCat + it.site.pageCat == profileSite.pageCat + it.site.page == profileSite.page + it.site.ref == profileSite.ref + it.site.search == profileSite.search + it.site.keywords == profileSite.keywords + + it.device.didsha1 == profileDevice.didsha1 + it.device.didmd5 == profileDevice.didmd5 + it.device.dpidsha1 == profileDevice.dpidsha1 + it.device.ifa == profileDevice.ifa + it.device.macsha1 == profileDevice.macsha1 + it.device.macmd5 == profileDevice.macmd5 + it.device.dpidmd5 == profileDevice.dpidmd5 + } + + and: "Bidder imp should contain data from specified profiles" + def firstBidderImpBanner = bidderRequest.imp.first.banner + verifyAll(firstBidderImpBanner) { + it.btype == firstImpProfile.body.banner.btype + it.battr == secondImpProfile.body.banner.battr + } + + and: "Ignore data from unspecified profiles" + assert !firstBidderImpBanner.mimes + + and: "Bidder imp should contain data from specified profiles" + def secondBidderImpBanner = bidderRequest.imp.last.banner + verifyAll(secondBidderImpBanner) { + it.battr == secondImpProfile.body.banner.battr + it.mimes == thirdImpProfile.body.banner.mimes + } + + and: "Ignore data from unspecified profiles" + assert !secondBidderImpBanner.btype + } + + def "PBS should count invalid or missing profiles towards the limit"() { + given: "Default bidRequest with request profiles" + def accountId = PBSUtils.randomNumber as String + def invalidProfileRequest = RequestProfile.getProfile(accountId).tap { + it.body = null + } + def impProfile = ImpProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.tap { + it.banner.format = [Format.randomFormat] + it.ext.prebid.profileNames = [impProfile.id] + } + it.ext.prebid.profileNames = [invalidProfileRequest.id, PBSUtils.randomString] + it.site = Site.configFPDSite + it.device = Device.default + setAccountId(accountId) + } + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(invalidProfileRequest)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.message.contains(LIMIT_ERROR_MESSAGE) + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + it.site.ext.data == bidRequest.site.ext.data + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + + and: "Bidder request imp should contain data from request" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + } + + def "PBS should include data from storedBidResponses when it specified in profiles"() { + given: "Default BidRequest with profile" + def accountId = PBSUtils.randomNumber as String + def storedResponseId = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null + it.body.ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid response" + assert response.id == bidRequest.id + assert response.seatbid[0]?.seat == storedBidResponse.seatbid[0].seat + assert response.seatbid[0]?.bid?.size() == storedBidResponse.seatbid[0].bid.size() + assert response.seatbid[0]?.bid[0]?.impid == storedBidResponse.seatbid[0].bid[0].impid + assert response.seatbid[0]?.bid[0]?.price == storedBidResponse.seatbid[0].bid[0].price + assert response.seatbid[0]?.bid[0]?.id == storedBidResponse.seatbid[0].bid[0].id + + and: "PBS not send request to bidder" + assert bidder.getRequestCount(bidRequest.id) == 0 + } + + def "PBS should include data from storedAuctionResponse when it specified in profiles"() { + given: "Default basic BidRequest with profile" + def accountId = PBSUtils.randomNumber as String + def storedAuctionId = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.id = null + it.body.ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedAuctionId) + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + and: "Stored response in DB" + def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest) + def storedResponse = new StoredResponse(responseId: storedAuctionId, + storedAuctionResponse: storedAuctionResponse) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should contain information from stored bid 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) == 0 + } + + def "PBS should fail auction when fail-on-unknown-profile enabled and profile is missing"() { + given: "PBS with profiles.fail-on-unknown config" + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + + ['auction.profiles.fail-on-unknown': 'true']) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': 'true']) + } + + def "PBS should fail auction when fail-on-unknown-profile default and profile is missing"() { + given: "PBS without profiles.fail-on-unknown config" + def prebidServerService = pbsServiceFactory.getService(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) + + and: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + when: "PBS processes auction request" + prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(PROFILES_CONFIG + ['auction.profiles.fail-on-unknown': null]) + } + + def "PBS should prioritise fail-on-unknown-profile from account over host config"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + it.site = new Site() + it.device = null + setAccountId(accountId) + } + + and: "Default account" + def accountAuctionConfig = new AccountAuctionConfig(profiles: profilesConfigs) + def accountConfig = new AccountConfig(auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, status: ACTIVE, config: accountConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid profile" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + where: + profilesConfigs << [ + new AccountProfilesConfigs(failOnUnknown: true), + new AccountProfilesConfigs(failOnUnknownSnakeCase: true), + ] + } + + def "PBS should ignore inner request profiles when stored request profile contain link for another profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def innerRequestProfile = RequestProfile.getProfile(accountId).tap { + it.body.app = App.defaultApp + } + + def requestProfile = RequestProfile.getProfile(accountId).tap { + it.body.ext.prebid.profileNames = [innerRequestProfile.id] + } + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]).tap { + it.site = Site.configFPDSite + it.device = Device.default + } as BidRequest + + and: "Default profiles in database" + profileRequestDao.save(StoredProfileRequest.getProfile(innerRequestProfile)) + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { + it.site.id == requestProfile.body.site.id + it.site.name == requestProfile.body.site.name + it.site.domain == requestProfile.body.site.domain + it.site.cat == requestProfile.body.site.cat + it.site.sectionCat == requestProfile.body.site.sectionCat + it.site.pageCat == requestProfile.body.site.pageCat + it.site.page == requestProfile.body.site.page + it.site.ref == requestProfile.body.site.ref + it.site.search == requestProfile.body.site.search + it.site.keywords == requestProfile.body.site.keywords + it.site.ext.data == requestProfile.body.site.ext.data + + it.device.didsha1 == requestProfile.body.device.didsha1 + it.device.didmd5 == requestProfile.body.device.didmd5 + it.device.dpidsha1 == requestProfile.body.device.dpidsha1 + it.device.ifa == requestProfile.body.device.ifa + it.device.macsha1 == requestProfile.body.device.macsha1 + it.device.macmd5 == requestProfile.body.device.macmd5 + it.device.dpidmd5 == requestProfile.body.device.dpidmd5 + } + + and: "Bidder request shouldn't contain data from inner profile" + assert !bidderRequest.app + } + + def "PBS should ignore inner imp profiles when stored imp profile contain link for another profile"() { + given: "Default bidRequest with imp profile" + def accountId = PBSUtils.randomNumber as String + def innerImpProfile = ImpProfile.getProfile(accountId, Imp.getDefaultImpression(VIDEO)) + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.ext.prebid.profileNames = [innerImpProfile.id] + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]).tap { + it.imp.first.banner = null + } as BidRequest + + and: "Default profiles in database" + profileImpDao.save(StoredProfileImp.getProfile(innerImpProfile)) + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imp should contain data from profile" + def bidderImp = bidder.getBidderRequest(bidRequest.id).imp.first + assert bidderImp.banner == impProfile.body.banner + + and: "Bidder request imp shouldn't contain data from inner profile" + assert bidderImp.video == impProfile.body.video + } + + def "PBS shouldn't validate profiles and imp before margining"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def height = PBSUtils.randomNumber + def impProfile = ImpProfile.getProfile(accountId).tap { + it.body.banner.format.first.width = null + it.body.banner.format.first.height = height + } + def bidRequest = getRequestWithProfiles(accountId, [impProfile]) as BidRequest + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBs should throw error due to invalid request" + 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' + } + + def "PBS shouldn't emit error or warnings when bidRequest contains multiple imps with same profile"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def imp = Imp.defaultImpression.tap { + it.banner.format = [Format.randomFormat] + } + def impProfile = ImpProfile.getProfile(accountId, imp) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + addImp(Imp.getDefaultImpression()) + setAccountId(accountId) + } as BidRequest + bidRequest.imp.each { + it.ext.prebid.profileNames = [impProfile.id] + } + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request imps should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.first.banner == impProfile.body.banner + assert bidder.getBidderRequest(bidRequest.id).imp.last.banner == impProfile.body.banner + } + + def "PBS should ignore imp data from request profile when imp for profile not null"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequestProfile = BidRequest.defaultBidRequest.tap { + it.id = null + it.imp.first.banner.format = [Format.randomFormat] + } + def requestProfile = RequestProfile.getProfile(accountId, + bidRequestProfile, + PBSUtils.randomString, + mergePrecedence) + def bidRequest = getRequestWithProfiles(accountId, [requestProfile]) + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors and warnings" + assert !response.ext?.errors + assert !response.ext?.warnings + + and: "Bidder request should contain data from profile" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + + where: + mergePrecedence << [REQUEST, PROFILE] + } + + @PendingFeature + def "PBS should add error and metrics when imp name is invalid"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def impProfile = ImpProfile.getProfile(accountId, Imp.defaultImpression, invalidProfileName) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [impProfile.id] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(impProfile)) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [LIMIT_ERROR_MESSAGE] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "PBS log should contain error" + assert pbsWithStoredProfiles.isContainLogsByValue(LIMIT_ERROR_MESSAGE) + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[LIMIT_EXCEEDED_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from original request" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site == bidRequest.site + it.device == bidRequest.device + } + + where: + invalidProfileName << [PBSUtils.randomSpecialChars, PBSUtils.randomStringWithSpecials] + } + + def "PBS should emit error and metrics when request profile called from imp level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = RequestProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [requestProfile.id] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Default profile in database" + profileRequestDao.save(StoredProfileRequest.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile called from request level"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def requestProfile = ImpProfile.getProfile(accountId) + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [requestProfile.id] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Default profile in database" + profileImpDao.save(StoredProfileImp.getProfile(requestProfile)) + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_PROFILE_MESSAGE.formatted(requestProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfileId] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + } + + def "PBS should emit error and metrics when request profile missing"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + } + + def "PBS should emit error and metrics when imp profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [invalidProfile.id] + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_IMP_PROFILE_MESSAGE.formatted(invalidProfile.id)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request imp should contain data from original imp" + assert bidder.getBidderRequest(bidRequest.id).imp.banner == bidRequest.imp.banner + + where: + invalidProfile << [ + ImpProfile.getProfile().tap { it.type = ProfileType.EMPTY }, + ImpProfile.getProfile().tap { it.type = ProfileType.UNKNOWN }, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY }, + ImpProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN }, + ] + } + + def "PBS should emit error and metrics when request profile have invalid data"() { + given: "Default bidRequest with request profile" + def accountId = PBSUtils.randomNumber as String + def invalidProfileId = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [invalidProfileId] + it.site = Site.getRootFPDSite() + it.device = Device.getDefault() + setAccountId(accountId) + } + + and: "Flash metrics" + flushMetrics(pbsWithStoredProfiles) + + when: "PBS processes auction request" + def response = pbsWithStoredProfiles.sendAuctionRequest(bidRequest) + + then: "PBS should emit proper warning" + assert response.ext?.warnings[ErrorType.PREBID]*.code == [999] + assert response.ext?.warnings[ErrorType.PREBID]*.message == [NO_REQUEST_PROFILE_MESSAGE.formatted(invalidProfileId)] + + and: "Response should contain error" + assert !response.ext?.errors + + and: "Missing metric should increments" + def metrics = pbsWithStoredProfiles.sendCollectedMetricsRequest() + assert metrics[MISSING_ACCOUNT_PROFILE_METRIC.formatted(accountId)] == 1 + + and: "Bidder request should contain data from profile" + verifyAll(bidder.getBidderRequest(bidRequest.id)) { + it.site.id == bidRequest.site.id + it.site.name == bidRequest.site.name + it.site.domain == bidRequest.site.domain + it.site.cat == bidRequest.site.cat + it.site.sectionCat == bidRequest.site.sectionCat + it.site.pageCat == bidRequest.site.pageCat + it.site.page == bidRequest.site.page + it.site.ref == bidRequest.site.ref + it.site.search == bidRequest.site.search + it.site.keywords == bidRequest.site.keywords + + it.device.didsha1 == bidRequest.device.didsha1 + it.device.didmd5 == bidRequest.device.didmd5 + it.device.dpidsha1 == bidRequest.device.dpidsha1 + it.device.ifa == bidRequest.device.ifa + it.device.macsha1 == bidRequest.device.macsha1 + it.device.macmd5 == bidRequest.device.macmd5 + it.device.dpidmd5 == bidRequest.device.dpidmd5 + } + + where: + invalidProfile << [ + RequestProfile.getProfile().tap { it.type = ProfileType.EMPTY }, + RequestProfile.getProfile().tap { it.type = ProfileType.UNKNOWN }, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.EMPTY }, + RequestProfile.getProfile().tap { it.mergePrecedence = ProfileMergePrecedence.UNKNOWN }, + ] + } + + def "PBS should throw exception when profiles are not configured and request contain profileId"() { + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(requestWithProfile) + + then: "PBs should throw error due to invalid profile config" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + CONFIG_ERROR_MESSAGE + + where: + requestWithProfile << [ + BidRequest.getDefaultBidRequest().tap { + it.imp.first.ext.prebid.profileNames = [PBSUtils.randomString] + }, + BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [PBSUtils.randomString] + } + ] + } + + def "PBS should throw exception when profiles are not configured for filesystem and request contain profileId"() { + given: "PBS with profiles.fail-on-unknown config" + def config = FILESYSTEM_CONFIG + PROFILES_CONFIG + ['settings.filesystem.profiles-dir': null] + pbsContainer = new PrebidServerContainer(config) + pbsContainer.withFolder(REQUESTS_PATH) + pbsContainer.withFolder(IMPS_PATH) + pbsContainer.withFolder(RESPONSES_PATH) + pbsContainer.withFolder(CATEGORIES_PATH) + def accountsConfig = new FileSystemAccountsConfig(accounts: [new AccountConfig(id: ACCOUNT_ID_FILE_STORAGE, status: ACTIVE)]) + pbsContainer.withCopyToContainer(Transferable.of(encodeYaml(accountsConfig)), + SETTINGS_FILENAME) + pbsContainer.start() + pbsWithStoredProfiles = new PrebidServerService(pbsContainer) + + and: "BidRequest with profile" + def requestWithProfile = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.profileNames = [PBSUtils.randomString] + } + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(requestWithProfile) + + then: "PBs should throw error due to invalid profile config" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == INVALID_REQUEST_PREFIX + CONFIG_ERROR_MESSAGE + + cleanup: "Stop and remove pbs container" + pbsContainer.stop() + } + + private static BidRequest getRequestWithProfiles(String accountId, List profiles) { + BidRequest.getDefaultBidRequest().tap { + if (profiles.type.contains(ProfileType.IMP)) { + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id + } + it.imp.first.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.IMP }*.id + it.ext.prebid.profileNames = profiles.findAll { it.type == ProfileType.REQUEST }*.id + setAccountId(accountId) + } + } +} 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..02adb2ab5e5 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,48 @@ package org.prebid.server.functional.tests import org.mockserver.model.HttpStatusCode +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.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.DebugCondition.DISABLED +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED +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 { @@ -36,26 +59,23 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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" @@ -65,18 +85,105 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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 == BidderName.GENERIC + 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.width = bidRequest.imp.first.banner.format.first.width + 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 == BidderName.GENERIC + 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 == BidderName.GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE + + and: "PBS response shouldn't contain seatBid" + assert !response.seatbid + } + + 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 +192,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 = [] } @@ -112,15 +218,15 @@ class SeatNonBidSpec extends BaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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 where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS shouldn't populate seatNonBid when returnAllBidStatus=false and debug=#debug and requested bidder didn't bid for any reason"() { @@ -146,7 +252,7 @@ class SeatNonBidSpec extends BaseSpec { assert !response.seatbid where: - debug << [1, 0, null] + debug << [ENABLED, DISABLED, null] } def "PBS should populate seatNonBid when bidder is rejected due to timeout"() { @@ -155,8 +261,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 } @@ -174,9 +279,9 @@ class SeatNonBidSpec extends BaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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"() { @@ -198,11 +303,39 @@ class SeatNonBidSpec extends BaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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/SetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy index b9e310c2b26..7e9eff9ebd3 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy @@ -3,19 +3,27 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.UidsCookie import org.prebid.server.functional.model.request.setuid.SetuidRequest import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo +import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils import org.prebid.server.functional.util.privacy.TcfConsent import org.prebid.server.util.ResourceUtil import spock.lang.Shared import java.time.Clock import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +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.APPNEXUS 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.request.setuid.UidWithExpiry.defaultUidWithExpiry import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer @@ -24,8 +32,11 @@ import static org.prebid.server.functional.util.privacy.TcfConsent.RUBICON_VENDO class SetUidSpec extends BaseSpec { private static final Integer MAX_COOKIE_SIZE = 500 + private static final Integer MAX_NUMBER_OF_UID_COOKIES = 30 + private static final Integer UPDATED_EXPIRE_DAYS = 14 private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final boolean CORS_SUPPORT = false + private static final Integer RANDOM_EXPIRE_DAY = PBSUtils.getRandomNumber(1, 10) private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" private static final Map PBS_CONFIG = ["host-cookie.max-cookie-size-bytes" : MAX_COOKIE_SIZE as String, @@ -37,9 +48,16 @@ class SetUidSpec extends BaseSpec { "adapters.${APPNEXUS.value}.usersync.cookie-family-name" : APPNEXUS.value, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + private static final Map UID_COOKIES_CONFIG = ['setuid.number-of-uid-cookies': MAX_NUMBER_OF_UID_COOKIES.toString()] + private static final Map GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true", + "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved" + private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451 @Shared - PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) + PrebidServerService singleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG) + @Shared + PrebidServerService multipleCookiesPbsService = pbsServiceFactory.getService(PBS_CONFIG + UID_COOKIES_CONFIG + GENERIC_ALIAS_CONFIG) def "PBS should set uids cookie"() { given: "Default SetuidRequest" @@ -47,36 +65,55 @@ class SetUidSpec extends BaseSpec { def uidsCookie = UidsCookie.defaultUidsCookie when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain uid cookie" + assert response.uidsCookie.tempUIDs[GENERIC].uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + } + + def "PBS should updated uids cookie when request parameters contain uid"() { + given: "Default SetuidRequest" + def requestUid = UUID.randomUUID().toString() + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = requestUid + } + def uidsCookie = UidsCookie.defaultUidsCookie + + and: "Flush metrics" + flushMetrics(singleCookiesPbsService) + + when: "PBS processes setuid request" + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookie" - assert !response.uidsCookie.tempUIDs - assert !response.uidsCookie.uids + assert daysDifference(response.uidsCookie.tempUIDs[GENERIC].expires) == UPDATED_EXPIRE_DAYS + assert response.uidsCookie.tempUIDs[GENERIC].uid == requestUid assert response.responseBody == ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + and: "usersync.FAMILY.sets metric should be updated" + def metrics = singleCookiesPbsService.sendCollectedMetricsRequest() + assert metrics["usersync.${GENERIC.value}.sets"] == 1 } def "PBS setuid should remove expired uids cookie"() { given: "Default SetuidRequest" def request = SetuidRequest.defaultSetuidRequest - def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires = ZonedDateTime.now(Clock.systemUTC()).minusDays(2) - } - tempUIDs = [(RUBICON): uidWithExpiry] - } + def uidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, -RANDOM_EXPIRE_DAY) when: "PBS processes setuid request" - def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + def response = singleCookiesPbsService.sendSetUidRequest(request, uidsCookie) then: "Response shouldn't contain uids cookie" - assert !response.uidsCookie.tempUIDs[RUBICON] + assert !response.uidsCookie.tempUIDs } def "PBS setuid should return requested uids cookie when priority bidder not present in config"() { given: "PBS config" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": null]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": null] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Setuid request" def request = SetuidRequest.defaultSetuidRequest.tap { @@ -89,16 +126,19 @@ class SetUidSpec extends BaseSpec { when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - then: "Response should contain requested uids" + then: "Response should contain requested tempUIDs" assert response.uidsCookie.tempUIDs[GENERIC] assert response.uidsCookie.tempUIDs[RUBICON] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS setuid should return prioritized uids bidder when size is full"() { given: "PBS config" def genericBidder = GENERIC - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": genericBidder.value]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": genericBidder.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Setuid request" def request = SetuidRequest.defaultSetuidRequest.tap { @@ -107,8 +147,8 @@ class SetUidSpec extends BaseSpec { } def rubiconBidder = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (rubiconBidder): defaultUidWithExpiry] + tempUIDs = [(APPNEXUS) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (rubiconBidder): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } when: "PBS processes setuid request" @@ -117,12 +157,15 @@ class SetUidSpec extends BaseSpec { then: "Response should contain uids cookies" assert response.uidsCookie.tempUIDs[rubiconBidder] assert response.uidsCookie.tempUIDs[genericBidder] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove earliest expiration bidder when size is full"() { + def "PBS setuid should remove most distant expiration bidder when size is full"() { given: "PBS config" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": GENERIC.value]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Setuid request" def request = SetuidRequest.defaultSetuidRequest.tap { @@ -143,14 +186,17 @@ class SetUidSpec extends BaseSpec { def response = prebidServerService.sendSetUidRequest(request, uidsCookie) then: "Response should contain uids cookies" - assert response.uidsCookie.tempUIDs[APPNEXUS] + assert response.uidsCookie.tempUIDs[RUBICON] assert response.uidsCookie.tempUIDs[GENERIC] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS setuid should ignore requested bidder and log metric when cookie's filled and requested bidder not in prioritize list"() { given: "PBS config" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": APPNEXUS.value]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": APPNEXUS.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Setuid request" def bidderName = GENERIC @@ -174,17 +220,19 @@ class SetUidSpec extends BaseSpec { and: "Response should contain uids cookies" assert response.uidsCookie.tempUIDs[APPNEXUS] assert response.uidsCookie.tempUIDs[RUBICON] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS setuid should reject bidder when cookie's filled and requested bidder in pri and rejected by tcf"() { given: "Setuid request" - def bidderName = RUBICON - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG - + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(), - "cookie-sync.pri" : bidderName.value]) + def pbsConfig = PBS_CONFIG + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(), + "cookie-sync.pri" : RUBICON.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) def request = SetuidRequest.defaultSetuidRequest.tap { - it.bidder = bidderName + it.bidder = RUBICON gdpr = "1" gdprConsent = new TcfConsent.Builder().build() } @@ -199,18 +247,21 @@ class SetUidSpec extends BaseSpec { then: "Request should fail with error" def exception = thrown(PrebidServerException) - assert exception.statusCode == 451 - assert exception.responseBody == "The gdpr_consent param prevents cookies from being saved" + assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE + assert exception.responseBody == TCF_ERROR_MESSAGE and: "usersync.FAMILY.tcf.blocked metric should be updated" def metric = prebidServerService.sendCollectedMetricsRequest() - assert metric["usersync.${bidderName.value}.tcf.blocked"] == 1 + assert metric["usersync.${RUBICON.value}.tcf.blocked"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS setuid should remove oldest uid and log metric when cookie's filled and oldest uid's not on the pri"() { + def "PBS setuid should remove most distant expiration uid and log metric when cookie's filled and this uid's not on the pri"() { given: "PBS config" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": GENERIC.value]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Flush metrics" flushMetrics(prebidServerService) @@ -220,31 +271,30 @@ class SetUidSpec extends BaseSpec { uid = UUID.randomUUID().toString() } - def bidderName = RUBICON def uidsCookie = UidsCookie.defaultUidsCookie.tap { - def uidWithExpiry = defaultUidWithExpiry.tap { - expires.plusDays(10) - } - tempUIDs = [(APPNEXUS) : defaultUidWithExpiry, - (bidderName): uidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1)] } when: "PBS processes setuid request" def response = prebidServerService.sendSetUidRequest(request, uidsCookie) - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metrics = prebidServerService.sendCollectedMetricsRequest() - assert metrics["usersync.${bidderName.value}.sizedout"] == 1 + assert metrics["usersync.${RUBICON.value}.sizeblocked"] == 1 then: "Response should contain uids cookies" assert response.uidsCookie.tempUIDs[APPNEXUS] assert response.uidsCookie.tempUIDs[GENERIC] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS SetUid should remove oldest bidder from uids cookie in favor of prioritized bidder"() { + def "PBS set uid should emit sizeblocked metric and remove most distant expiration bidder from uids cookie for non-prioritized bidder"() { given: "PBS config" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + - ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String]) + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Set uid request" def request = SetuidRequest.defaultSetuidRequest.tap { @@ -254,8 +304,8 @@ class SetUidSpec extends BaseSpec { and: "Set up set uid cookie" def uidsCookie = UidsCookie.defaultUidsCookie.tap { - it.tempUIDs = [(APPNEXUS): defaultUidWithExpiry, - (RUBICON) : defaultUidWithExpiry] + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] } and: "Flush metrics" @@ -268,16 +318,214 @@ class SetUidSpec extends BaseSpec { assert response.uidsCookie.tempUIDs[OPENX] and: "Response set cookie header size should be lowest or the same as max cookie config size" - assert response.headers.get("Set-Cookie").split("Secure;")[0].length() <= MAX_COOKIE_SIZE + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE and: "Request bidder should contain uid from Set uid request" assert response.uidsCookie.tempUIDs[OPENX].uid == request.uid - and: "usersync.FAMILY.sizedout metric should be updated" + and: "usersync.FAMILY.sizeblocked metric should be updated" def metricsRequest = prebidServerService.sendCollectedMetricsRequest() - assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + assert metricsRequest["usersync.${APPNEXUS.value}.sizeblocked"] == 1 and: "usersync.FAMILY.sets metric should be updated" assert metricsRequest["usersync.${OPENX.value}.sets"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS set uid should emit sizedout metric and remove most distant expiration bidder from uids cookie in prioritized bidder"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $APPNEXUS.value, $RUBICON.value" as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Set uid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Set up set uid cookie" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + tempUIDs = [(APPNEXUS): getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY + 1), + (OPENX) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY), + (RUBICON) : getDefaultUidWithExpiry(RANDOM_EXPIRE_DAY)] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + when: "PBS processes set uid request" + def response = prebidServerService.sendSetUidRequest(request, uidsCookie) + + then: "Response should contain pri bidder in uids cookies" + assert response.uidsCookie.tempUIDs[OPENX] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Response set cookie header size should be lowest or the same as max cookie config size" + assert getSetUidsHeaders(response).first.split("Secure;")[0].length() <= MAX_COOKIE_SIZE + + and: "usersync.FAMILY.sizedout metric should be updated" + def metricsRequest = prebidServerService.sendCollectedMetricsRequest() + assert metricsRequest["usersync.${APPNEXUS.value}.sizedout"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS setuid should reject request when requested bidder mismatching with cookie-family-name"() { + given: "Default SetuidRequest" + def request = SetuidRequest.getDefaultSetuidRequest().tap { + it.bidder = bidderName + } + + when: "PBS processes setuid request" + singleCookiesPbsService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 400 + assert exception.responseBody == 'Invalid request format: "bidder" query param is invalid' + + where: + bidderName << [UNKNOWN, WILDCARD, GENERIC_CAMEL_CASE, ALIAS, ALIAS_CAMEL_CASE] + } + + def "PBS should throw an exception when incoming request have optout flag"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + and: "PBS service with optout cookies" + def pbsConfig = PBS_CONFIG + ["host-cookie.optout-cookie.name" : "uids", + "host-cookie.optout-cookie.value": Base64.urlEncoder.encodeToString(encode(genericUidsCookie).bytes)] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(request, [genericUidsCookie]) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == 401 + assert exception.responseBody == 'Unauthorized: Sync is not allowed for this uids' + } + + def "PBS should merge cookies when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + def setUidsHeaders = getSetUidsHeaders(response) + def uidsCookie = extractHeaderTempUIDs(setUidsHeaders.first) + assert setUidsHeaders.size() == 1 + assert uidsCookie.tempUIDs[GENERIC] + assert uidsCookie.tempUIDs[RUBICON] + } + + def "PBS should send multiple uids cookies by priority and expiration timestamp"() { + given: "PBS config" + def pbsConfig = PBS_CONFIG + + UID_COOKIES_CONFIG + + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String] + + ["host-cookie.max-cookie-size-bytes": MAX_COOKIE_SIZE as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + + and: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON, RANDOM_EXPIRE_DAY + 2) + def openxUidsCookie = UidsCookie.getDefaultUidsCookie(OPENX, RANDOM_EXPIRE_DAY + 3) + def appnexusUidsCookie = UidsCookie.getDefaultUidsCookie(APPNEXUS, RANDOM_EXPIRE_DAY) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(request, [appnexusUidsCookie, genericUidsCookie, rubiconUidsCookie, openxUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs.keySet() == new LinkedHashSet([GENERIC, OPENX, APPNEXUS, RUBICON]) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should remove duplicates when incoming cookie-family already exists in the working list"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest + + and: "Duplicated uids cookies" + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY) + def duplicateUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC, RANDOM_EXPIRE_DAY + 1) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, duplicateUidsCookie]) + + then: "Response should contain single generic uid with most distant expiration timestamp" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == duplicateUidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == duplicateUidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should shouldn't modify uids cookie when uid is empty"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + it.uid = null + it.bidder = GENERIC + } + + and: "Specific uids cookies" + def uidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [uidsCookie]) + + then: "Response should contain single generic uid" + assert response.uidsCookie.tempUIDs.size() == 1 + assert response.uidsCookie.tempUIDs[GENERIC].uid == uidsCookie.tempUIDs[GENERIC].uid + assert response.uidsCookie.tempUIDs[GENERIC].expires == uidsCookie.tempUIDs[GENERIC].expires + } + + def "PBS should include all cookies even empty when incoming request have multiple uids cookies"() { + given: "Setuid request" + def request = SetuidRequest.defaultSetuidRequest.tap { + uid = UUID.randomUUID().toString() + } + def genericUidsCookie = UidsCookie.getDefaultUidsCookie(GENERIC) + def rubiconUidsCookie = UidsCookie.getDefaultUidsCookie(RUBICON) + + when: "PBS processes setuid request" + def response = multipleCookiesPbsService.sendSetUidRequest(request, [genericUidsCookie, rubiconUidsCookie]) + + then: "Response should contain requested tempUIDs" + assert response.uidsCookie.tempUIDs[GENERIC] + assert response.uidsCookie.tempUIDs[RUBICON] + + and: "Headers uids cookies should contain same cookie as response" + assert getSetUidsHeaders(response).size() == 1 + assert getSetUidsHeaders(response, true).size() == MAX_NUMBER_OF_UID_COOKIES + } + + List getSetUidsHeaders(SetuidResponse response, boolean includeEmpty = false) { + response.headers.get("Set-Cookie").findAll { cookie -> + includeEmpty || !(cookie =~ /\buids\d*=\s*;/) + } + } + + static UidsCookie extractHeaderTempUIDs(String header) { + def uid = (header =~ /uids\d*=(\S+?);/)[0][1] + decodeWithBase64(uid as String, UidsCookie) + } + + def daysDifference(ZonedDateTime inputDate) { + ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC()).minusHours(1) + return ChronoUnit.DAYS.between(now, inputDate) } } 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..daa27b260fa 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 { @@ -99,7 +98,7 @@ class SmokeSpec extends BaseSpec { def accountId = PBSUtils.randomNumber.toString() when: "PBS processes vtrack request" - def response = defaultPbsService.sendVtrackRequest(request, accountId) + def response = defaultPbsService.sendPostVtrackRequest(request, accountId) then: "Response should contain uid" assert response.responses[0]?.uuid 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..0b6f62923c7 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,15 @@ 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.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import spock.lang.PendingFeature @@ -13,6 +18,8 @@ import static org.prebid.server.functional.model.bidder.BidderName.GENERIC class StoredResponseSpec extends BaseSpec { + private final PrebidServerService pbsService = pbsServiceFactory.getService(["cache.default-ttl-seconds.banner": ""]) + @PendingFeature def "PBS should not fail auction with storedAuctionResponse when request bidder params doesn't satisfy json-schema"() { given: "BidRequest with bad bidder datatype and storedAuctionResponse" @@ -29,7 +36,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should not contain errors and warnings" assert !response.ext?.errors @@ -52,7 +59,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored auction response" assert response.id == bidRequest.id @@ -78,7 +85,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response" assert response.id == bidRequest.id @@ -107,7 +114,7 @@ class StoredResponseSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsService.sendAuctionRequest(bidRequest) then: "Response should contain information from stored bid response and change bid.impId on imp.id" assert response.id == bidRequest.id @@ -120,4 +127,259 @@ 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 = pbsService.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 = pbsService.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 = pbsService.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 = pbsService.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 = pbsService.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 = pbsService.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" + pbsService.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" + pbsService.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 = pbsService.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 = pbsService.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 = pbsService.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 List convertToComparableSeatBid(List seatBids) { + seatBids*.tap { seatBid -> + seatBid.bid*.tap { bid -> + bid.ext = null + bid.price = bid.price.setScale(3) + } + seatBid.group = null + } + seatBids + } } 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..d9d337b3c27 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,36 +4,83 @@ 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.AccountRankingConfig +import org.prebid.server.functional.model.config.PriceGranularityType 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.AdServerTargeting 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.MultiBid +import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidCache +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.model.request.auction.PriceGranularity import org.prebid.server.functional.model.request.auction.Range +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting +import org.prebid.server.functional.model.request.auction.Video import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +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.Prebid +import org.prebid.server.functional.model.response.auction.SeatBid +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.testcontainers.scaffolding.Bidder 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.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +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.model.response.auction.MediaType.VIDEO 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 Integer MAX_BIDS_RANKING = 3 + private static final String HB_ENV_AMP = "amp" + private static final Integer MAIN_RANK = 1 + private static final Integer SUBORDINATE_RANK = 2 + private static final String EMPTY_CPM = "0.0" + private static final Integer DEFAULT_TRUNCATE_CHARS = 20 + private static final Integer EXTENDED_TRUNCATE_CHARS = PbsConfig.targetingConfig.get('settings.targeting.truncate-attr-chars').toInteger() + private static final Map EMPTY_TARGETING_CONFIG = ['settings.targeting.truncate-attr-chars': null] as Map + private static final Map ONLY_WINNING_BIDS_CONFIG = ["auction.cache.only-winning-bids": "true"] + private static final Map DISABLED_ONLY_WINNING_BIDS_CONFIG = ["auction.cache.only-winning-bids": "false"] + private static final String DROP_PREFIX_WARNING = "Key prefix value is dropped to default. " + + "Decrease custom prefix length or increase truncateattrchars by %s" + private static final String TRUNCATED_WARNING = "The following keys have been truncated:" + + private static final PrebidServerService pbsWithDefaultTargetingLength = pbsServiceFactory.getService(EMPTY_TARGETING_CONFIG) + private static final PrebidServerService pbsWithOnlyWinningBids = pbsServiceFactory.getService(EMPTY_TARGETING_CONFIG + ONLY_WINNING_BIDS_CONFIG) + private static final PrebidServerService pbsWithDisabledOnlyWinningBids = pbsServiceFactory.getService(EMPTY_TARGETING_CONFIG + DISABLED_ONLY_WINNING_BIDS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(EMPTY_TARGETING_CONFIG + ONLY_WINNING_BIDS_CONFIG) + pbsServiceFactory.removeContainer(EMPTY_TARGETING_CONFIG + DISABLED_ONLY_WINNING_BIDS_CONFIG) + pbsServiceFactory.removeContainer(EMPTY_TARGETING_CONFIG) + } def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -55,7 +102,7 @@ class TargetingSpec extends BaseSpec { def bidderName = GENERIC.value when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "PBS response targeting contains bidder specific keys" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -81,7 +128,7 @@ class TargetingSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "PBS response targeting contains bidder specific keys" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -106,7 +153,7 @@ class TargetingSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = getEnabledWinBidsPbsService().sendAuctionRequest(bidRequest) + def response = pbsWithOnlyWinningBids.sendAuctionRequest(bidRequest) then: "PBS response targeting does not contain bidder specific keys" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -134,7 +181,7 @@ class TargetingSpec extends BaseSpec { def bidderName = GENERIC.value when: "PBS processes auction request" - def response = getDisabledWinBidsPbsService().sendAuctionRequest(bidRequest) + def response = pbsWithDisabledOnlyWinningBids.sendAuctionRequest(bidRequest) then: "PBS response targeting contains bidder specific keys" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -161,7 +208,7 @@ class TargetingSpec extends BaseSpec { } when: "Requesting PBS auction" - def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) + def bidResponse = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "PBS response shouldn't contain targeting in response" assert !bidResponse.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -185,7 +232,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Amp response shouldn't contain targeting" assert !response.targeting @@ -207,7 +254,7 @@ class TargetingSpec extends BaseSpec { bidder.setResponse(bidRequest.id, bidResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "PBS response targeting includes only one deal specific key" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting @@ -236,7 +283,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Bidder request should contain amp query params in ext.prebid.amp.data" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) @@ -298,7 +345,7 @@ class TargetingSpec extends BaseSpec { storedResponseDao.save(storedResponse) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Amp response targeting should contain ad server targeting key" verifyAll { @@ -330,7 +377,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Amp response shouldn't contain custom targeting" assert !response.targeting[customKey] @@ -364,7 +411,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Amp response shouldn't contain custom targeting with full naming" assert !response.targeting[customKey] @@ -378,7 +425,7 @@ class TargetingSpec extends BaseSpec { def pbsConfig = [ "adapters.openx.enabled" : "true", "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] - def defaultPbsService = pbsServiceFactory.getService(pbsConfig) + def pbsWithDefaultTargetingLength = pbsServiceFactory.getService(pbsConfig) and: "Default bid request" def accountId = PBSUtils.randomNumber as String @@ -390,18 +437,21 @@ 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) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "Response should contain targeting with corresponding length" assert response.seatbid.bid.ext.prebid.targeting .every(list -> list .every(map -> map.keySet() .every(key -> key.length() <= targetingLength))) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should truncate targeting corresponding to value in account config when in account define truncate target attr"() { @@ -417,7 +467,7 @@ class TargetingSpec extends BaseSpec { accountDao.save(account) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Response should contain in targeting key not biggest that max size define in account" assert response.targeting.keySet().every { str -> str.length() <= MAX_AMP_TARGETING_TRUNCATION_LENGTH } @@ -435,11 +485,11 @@ 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" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Response shouldn't contain targeting" assert response.targeting.isEmpty() @@ -458,7 +508,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)] + } } } @@ -468,7 +519,7 @@ class TargetingSpec extends BaseSpec { and: "Create and save stored response into DB" def storedBidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap { - seatbid[0].bid[0].price = max.plus(1) + seatbid[0].bid[0].price = max + 1 } def storedResponse = new StoredResponse(responseId: storedBidResponseId, storedBidResponse: storedBidResponse) storedResponseDao.save(storedResponse) @@ -478,7 +529,7 @@ class TargetingSpec extends BaseSpec { accountDao.save(account) when: "PBS processes amp request" - def response = defaultPbsService.sendAmpRequest(ampRequest) + def response = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Response should contain targeting hb_pb" assert response.targeting["hb_pb"] == String.format("%,.2f", max.setScale(precision, RoundingMode.DOWN)) @@ -501,19 +552,43 @@ class TargetingSpec extends BaseSpec { and: "Create and save stored response into DB" def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { - seatbid[0].bid[0].price = max.plus(1) + seatbid[0].bid[0].price = max + 1 } def storedResponse = new StoredResponse(responseId: storedBidResponseId, storedBidResponse: storedBidResponse) storedResponseDao.save(storedResponse) when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) then: "Response should contain targeting hb_pb" def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting assert targetingKeyMap["hb_pb"] == String.format("%,.2f", max.setScale(precision, RoundingMode.DOWN)) } + def "PBS auction shouldn't delete bid and update targeting if price equal zero and dealId present"() { + given: "Default bid request with stored response" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + } + + and: "Bid response with zero price" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = 0 + seatbid[0].bid[0].dealid = PBSUtils.randomString + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Response should contain proper targeting hb_pb" + def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting + assert targetingKeyMap["hb_pb"] == EMPTY_CPM + assert targetingKeyMap["hb_pb_generic"] == EMPTY_CPM + } + def "PBS auction should use default targeting prefix when ext.prebid.targeting.prefix is biggest that twenty"() { given: "Bid request with long targeting prefix" def prefix = PBSUtils.getRandomString(30) @@ -522,12 +597,12 @@ class TargetingSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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,16 +614,16 @@ 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" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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"() { @@ -558,14 +633,15 @@ class TargetingSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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,18 +652,19 @@ 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" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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"() { @@ -598,12 +675,12 @@ class TargetingSpec extends BaseSpec { } when: "PBS processes auction request" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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,15 +692,15 @@ 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" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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,15 +712,15 @@ 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" - def response = defaultPbsService.sendAuctionRequest(bidRequest) + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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"() { @@ -661,12 +738,12 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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 +756,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" @@ -687,12 +764,12 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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,18 +785,19 @@ 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" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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"() { @@ -736,14 +814,15 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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,16 +839,16 @@ 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" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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"() { @@ -787,12 +866,12 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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,16 +890,16 @@ 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" - def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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 +909,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 { @@ -845,7 +924,7 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - defaultPbsService.sendAmpRequest(ampRequest) + pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) then: "Amp response should contain value from targeting in imp.ext.data" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) @@ -853,12 +932,7 @@ 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) - - and: "Default AmpRequest" + given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest and: "Bid request" @@ -869,31 +943,26 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) and: "Account in the DB" - def prefix = PBSUtils.getRandomString(prefixMaxChars - TARGETING_PREFIX_LENGTH) + def prefix = PBSUtils.getRandomString(DEFAULT_TRUNCATE_CHARS - TARGETING_PREFIX_LENGTH) def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes amp request" - def ampResponse = prebidServerService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) - - and: "Default AmpRequest" + given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest and: "Bid request with prefix" - def prefix = PBSUtils.getRandomString(prefixMaxChars - TARGETING_PREFIX_LENGTH) + def prefix = PBSUtils.getRandomString(EXTENDED_TRUNCATE_CHARS - TARGETING_PREFIX_LENGTH) def ampStoredRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(prefix: prefix) } @@ -903,68 +972,53 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = prebidServerService.sendAmpRequest(ampRequest) + def ampResponse = defaultPbsService.sendAmpRequest(ampRequest) 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) - - and:"Bid request with prefix" - def prefix = PBSUtils.getRandomString(prefixMaxChars - TARGETING_PREFIX_LENGTH) + given: "Bid request with prefix" + def prefix = PBSUtils.getRandomString(EXTENDED_TRUNCATE_CHARS - TARGETING_PREFIX_LENGTH) def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(prefix: prefix) } when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": prefixMaxChars as String]) - - and: "Bid request with empty targeting" + given: "Bid request with empty targeting" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting() } and: "Account in the DB" - def prefix = PBSUtils.getRandomString(prefixMaxChars - TARGETING_PREFIX_LENGTH) + def prefix = PBSUtils.getRandomString(EXTENDED_TRUNCATE_CHARS - TARGETING_PREFIX_LENGTH) def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) + def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest) 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) - - and: "Default AmpRequest" + given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest and: "Bid request" @@ -975,33 +1029,28 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) and: "Account in the DB" - def prefix = PBSUtils.getRandomString(targetingChars + 1) + def prefix = PBSUtils.getRandomString(DEFAULT_TRUNCATE_CHARS + 1) def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) def account = new Account(uuid: ampRequest.account, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes amp request" - def ampResponse = prebidServerService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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}"] + def decreasePrefixLength = prefix.length() + TARGETING_PREFIX_LENGTH - DEFAULT_TRUNCATE_CHARS + assert ampResponse.ext?.warnings[TARGETING]*.message == [DROP_PREFIX_WARNING.formatted(decreasePrefixLength), truncatedMessage()] + } 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) - - and: "Default AmpRequest" + given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest and: "Bid request with prefix" - def prefix = PBSUtils.getRandomString(targetingChars) + def prefix = PBSUtils.getRandomString(DEFAULT_TRUNCATE_CHARS) def ampStoredRequest = BidRequest.defaultBidRequest.tap { - ext.prebid.targeting = new Targeting(prefix: PBSUtils.getRandomString(targetingChars)) + ext.prebid.targeting = new Targeting(prefix: prefix) } and: "Create and save stored request into DB" @@ -1009,73 +1058,867 @@ class TargetingSpec extends BaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - def ampResponse = prebidServerService.sendAmpRequest(ampRequest) + def ampResponse = pbsWithDefaultTargetingLength.sendAmpRequest(ampRequest) 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}"] + def decreasePrefixLength = prefix.length() + TARGETING_PREFIX_LENGTH - DEFAULT_TRUNCATE_CHARS + assert ampResponse.ext?.warnings[TARGETING]*.message == [DROP_PREFIX_WARNING.formatted(decreasePrefixLength), truncatedMessage()] } 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) - def prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) - - and:"Bid request with prefix" - def prefixSize = targetingChars + 1 + given: "Bid request with prefix" + def prefixSize = DEFAULT_TRUNCATE_CHARS + 1 def prefix = PBSUtils.getRandomString(prefixSize) def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting(prefix: prefix) } when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) + def bidResponse = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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 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}"] + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } + assert bidResponse.ext?.warnings[TARGETING]*.message == [DROP_PREFIX_WARNING.formatted(prefix.length() + TARGETING_PREFIX_LENGTH - DEFAULT_TRUNCATE_CHARS)] } 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 prebidServerService = pbsServiceFactory.getService( - ["settings.targeting.truncate-attr-chars": targetingChars as String]) - - and: "Bid request" + given: "Bid request" def bidRequest = BidRequest.defaultBidRequest.tap { ext.prebid.targeting = new Targeting() } and: "Account in the DB" - def prefix = PBSUtils.getRandomString(targetingChars + 1) + def prefix = PBSUtils.getRandomString(DEFAULT_TRUNCATE_CHARS + 1) def config = new AccountAuctionConfig(targeting: new Targeting(prefix: prefix)) def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: config)) accountDao.save(account) when: "PBS processes auction request" - def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) + def bidResponse = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) 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 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}"] + assert targeting.keySet().every { it -> it.startsWith(DEFAULT_TARGETING_PREFIX) } + assert bidResponse.ext?.warnings[TARGETING]*.message == [DROP_PREFIX_WARNING.formatted(prefix.length() + TARGETING_PREFIX_LENGTH - DEFAULT_TRUNCATE_CHARS)] + } + + 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 + pbsWithDefaultTargetingLength.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 = pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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: "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) + + and: "PBS with default account" + def pbsConfig = ["settings.default-account-config": encode(accountConfig)] + def pbsService = pbsServiceFactory.getService(pbsConfig) + + 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) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + 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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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" + pbsWithDefaultTargetingLength.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 "PBS shouldn't add bid ranked for request when account config for auction.ranking disabled or default"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + 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) + + and: "Bid response with 3 bids where deal bid has higher price" + def imp = bidRequest.imp.first + def bids = [Bid.getDefaultBid(imp), Bid.getDefaultBid(imp), Bid.getDefaultBid(imp)] + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = bids + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)) + ] + } + + def "PBS should add bid ranked and rank by deals for default request when auction.ranking and preferDeals are enabled"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidWithDeal.id] + it.price == [bidWithDeal.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for default request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDealId = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDealId] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidBiggerPrice.id] + it.price == [bidBiggerPrice.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for request with multiBid when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for multiple media types request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.video = Video.getDefaultVideo() + it.imp.first.nativeObj = Native.getDefaultNative() + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).first.tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).last.tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + assert !response.ext.warnings + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should properly rank bids when request with multibid contains some invalid bid"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def higherPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 2 + } + + def middlePriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + adm = null + } + + def lowerPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [lowerPriceBid, middlePriceBid, higherPriceBid] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == higherPriceBid.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == lowerPriceBid.id).ext.prebid.rank == 2 + + and: "PBS should contain error for invalid bid" + response.ext.errors[ErrorType.GENERIC]?.message == + ["BidId `${middlePriceBid.id}` validation messages: Error: Bid \"${middlePriceBid.id}\" with video type missing adm and nurl"] + } + + def "PBS should assign bid ranks across all seatbids combined when the request contains imps with multiple bidders"() { + given: "PBS config with openX bidder" + def endpoint = '/openx-auction' + def pbsConfig = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri$endpoint".toString()] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + def openxBidder = new Bidder(networkServiceContainer, endpoint) + + and: "Bid request with multiple bidders" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + it.ext.prebid.multibid = [new MultiBid(bidder: WILDCARD, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def genericBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def openxBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponseGeneric = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [genericBid], seat: GENERIC)] + } + def bidResponseOpenx = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [openxBid], seat: OPENX)] + } + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponseGeneric) + openxBidder.setResponse(bidRequest.id, bidResponseOpenx) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should rank OpenX bid higher than Generic bid" + assert response.seatbid.findAll { it.seat == OPENX }.bid.ext.prebid.rank.flatten() == [MAIN_RANK] + assert response.seatbid.findAll { it.seat == GENERIC }.bid.ext.prebid.rank.flatten() == [SUBORDINATE_RANK] + + cleanup: "Stop and remove pbs container and bidder response" + pbsServiceFactory.removeContainer(pbsConfig) + openxBidder.reset() + } + + def "PBS should assign bid ranks for each imp separately when request has multiple imps and multiBid is configured"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.nativeObj = Native.getDefaultNative() + imp.add(Imp.getDefaultImpression(VIDEO)) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = requestPreferDeals + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def bidLowerPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + mediaType = BidMediaType.NATIVE + } + def bidHigherPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp.last).tap { + dealid = PBSUtils.randomNumber + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidLowerPrice, bidHigherPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank bids for first imp" + def bids = response.seatbid.first.bid + def firstImpBidders = bids.findAll { it.impid == bidRequest.imp.id.first() } + assert firstImpBidders.find { it.id == bidHigherPrice.id }.ext.prebid.rank == 1 + assert firstImpBidders.find { it.id == bidLowerPrice.id }.ext.prebid.rank == 2 + + and: "should separately rank bids for second imp" + def secondImpBidders = bids.findAll { it.impid == bidRequest.imp.id.last() } + assert secondImpBidders*.ext.prebid.rank == [MAIN_RANK] + + where: + requestPreferDeals << [null, false, true] + } + + def "PBS should ignore bid ranked from original response when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for request with stored imp when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(impression).first.tap { + it.price = bidPrice + 1 + impid = bidRequest.imp.id.first + } + def bidBDeal = Bid.getDefaultMultiTypesBids(impression).last.tap { + impid = bidRequest.imp.id.first + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS shouldn't rank bids for request with stored imp when auction.ranking default"() { + given: "Bid request with enabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: new AccountAuctionConfig()) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + nativeObj = Native.getDefaultNative() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = Bid.getDefaultMultiTypesBids(impression) { impid = bidRequest.imp.id.first } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + } + + def "PBS should copy bid ranked from stored response when auction.ranking #auction"() { + given: "Bid request with enabled preferDeals" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: auction) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Stored response in DB" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPriceRanking = PBSUtils.randomNumber + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: bidBiggerPriceRanking)) + } + def bidBDealRanking = PBSUtils.randomNumber + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: bidBDealRanking)) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: new SeatBid(bid: [bidBiggerPrice, bidBDeal], seat: GENERIC)) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = pbsWithDefaultTargetingLength.sendAuctionRequest(bidRequest) + + then: "PBS should copy bid ranked from stored response" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == bidBiggerPriceRanking + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == bidBDealRanking + + where: + auction << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: true)) + ] + } + + private static Account createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) } - private PrebidServerService getEnabledWinBidsPbsService() { - pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) + private static Account getAccountConfigWithAuctionRanking(String accountId, Boolean auctionRankingEnablement = true) { + def accountAuctionConfig = new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: auctionRankingEnablement)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) } - private PrebidServerService getDisabledWinBidsPbsService() { - pbsServiceFactory.getService(["auction.cache.only-winning-bids": "false"]) + private static def truncatedMessage(List keys = ["hb_cache_host_${GENERIC}", "hb_cache_path_${GENERIC}"]) { + "$TRUNCATED_WARNING ${keys.join(', ')}" } } 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..cff45989db1 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,10 +15,11 @@ 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] + private static final Long MAX_AUCTION_BIDDER_TIMEOUT = 3000 + private static final Long MIN_AUCTION_BIDDER_TIMEOUT = 1000 + private static final Map PBS_CONFIG = ["auction.biddertmax.max": MAX_AUCTION_BIDDER_TIMEOUT as String, + "auction.biddertmax.min": MIN_AUCTION_BIDDER_TIMEOUT as String] @Shared PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG) @@ -139,9 +138,10 @@ class TimeoutSpec extends BaseSpec { and: "Pbs config with default request" def pbsContainer = new PrebidServerContainer( - ["default-request.file.path" : APP_WORKDIR + defaultRequest.fileName, - "auction.biddertmax.max" : MAX_TIMEOUT as String]).tap { - withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR) } + ["default-request.file.path": APP_WORKDIR + defaultRequest.fileName, + "auction.biddertmax.max" : MAX_TIMEOUT as String]).tap { + withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR) + } pbsContainer.start() def pbsService = new PrebidServerService(pbsContainer) @@ -284,127 +284,141 @@ 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]) + 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) + def prebidServerService = pbsServiceFactory.getService( + ["auction.biddertmax.min": minBidderTmax as String, + "auction.biddertmax.max": MAX_TIMEOUT as String]) - and: "Default AMP request without timeout" - def ampRequest = AmpRequest.defaultAmpRequest.tap { - timeout = null + and: "Default basic BidRequest" + def timeout = PBSUtils.getRandomNumber(0, minBidderTmax) + def bidRequest = BidRequest.defaultBidRequest.tap { + tmax = timeout } - and: "Default stored request tmax" - def minTmax = MIN_TIMEOUT_BIDDER_REQUEST - 1 - def ampStoredRequest = BidRequest.defaultStoredRequest.tap { - tmax = minTmax - } + when: "PBS processes auction request with warmup" + def bidResponse = prebidServerService.withWarmup().sendAuctionRequest(bidRequest) - and: "Save storedRequest into DB" - def storedRequestModel = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) - storedRequestDao.save(storedRequestModel) + then: "Bidder request should contain min value from auction.biddertmax.min config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.tmax == minBidderTmax as Long - when: "PBS processes amp request" - def bidResponse = prebidServerService.sendAmpRequest(ampRequest) + and: "PBS response should contain tmax from request" + assert bidResponse?.ext?.tmaxrequest == timeout as Long + } - then: "Bidder request timeout should correspond to the min from the stored request" - assert bidResponse?.ext?.debug?.resolvedRequest?.tmax == minTmax + def "PBS should change timeout for bidder due to percent in auction.biddertmax.percent"() { + given: "PBS config with percent" + def percent = PBSUtils.getRandomNumber(2, 98) + def pbsConfig = ["auction.biddertmax.percent": percent as String, + "auction.biddertmax.max" : MAX_TIMEOUT as String, + "auction.biddertmax.min" : MIN_TIMEOUT as String] + def prebidServerService = pbsServiceFactory.getService( + pbsConfig) - 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: "Default basic BidRequest with generic bidder" + def timeout = randomTimeout + def bidRequest = BidRequest.defaultBidRequest.tap { + tmax = timeout + } + + when: "PBS processes auction request with warmup" + def bidResponse = prebidServerService.withWarmup().sendAuctionRequest(bidRequest) + + then: "Bidder request should contain percent of request value" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent, timeout)) + + and: "PBS response should contain tmax from request" + assert bidResponse?.ext?.tmaxrequest == timeout as Long - 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"] + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - 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]) + def "PBS should apply auction.biddertmax.max timeout when adapters.generic.tmax-deduction-ms exceeds valid top range"() { + given: "PBS config with adapters.generic.tmax-deduction-ms" + def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": PBSUtils.getRandomNumber(MAX_AUCTION_BIDDER_TIMEOUT as int) as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) - and: "Default BidRequest without timeout" + and: "Default basic BidRequest with generic bidder" 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 + tmax = randomTimeout } - 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 + then: "Bidder request should contain min" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.tmax == MIN_AUCTION_BIDDER_TIMEOUT - 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: "PBS response should contain tmax" + assert bidResponse?.ext?.tmaxrequest == MAX_AUCTION_BIDDER_TIMEOUT - 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"] + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - 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) - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.min" : minBidderTmax as String, - "auction.biddertmax.max" : MAX_TIMEOUT as String]) + def "PBS should resolve timeout as usual when adapters.generic.tmax-deduction-ms specifies zero"() { + given: "PBS config with adapters.generic.tmax-deduction-ms" + def pbsConfig = ["adapters.generic.tmax-deduction-ms": "0"] + PBS_CONFIG + def prebidServerService = pbsServiceFactory.getService(pbsConfig) - and: "Default basic BidRequest" - def timeout = PBSUtils.getRandomNumber(0, minBidderTmax) + and: "Default basic BidRequest with generic bidder" + def timeout = PBSUtils.getRandomNumber( + MIN_AUCTION_BIDDER_TIMEOUT as int, + MAX_AUCTION_BIDDER_TIMEOUT as int) def bidRequest = BidRequest.defaultBidRequest.tap { tmax = timeout } - when: "PBS processes auction request with warmup" - def bidResponse = prebidServerService.withWarmup().sendAuctionRequest(bidRequest) + when: "PBS processes auction request" + def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) - then: "Bidder request should contain min value from auction.biddertmax.min config" + then: "Bidder request should contain right value in tmax" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.tmax == minBidderTmax as Long + assert isInternalProcessingTime(bidderRequest.tmax, timeout) - and: "PBS response should contain tmax from request" + and: "PBS response should contain tmax" assert bidResponse?.ext?.tmaxrequest == timeout as Long + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } - def "PBS should change timeout for bidder due to percent in auction.biddertmax.percent"() { - given: "PBS config with percent" - def percent = PBSUtils.getRandomNumber(2, 98) - def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.percent": percent as String] - + PBS_CONFIG) + def "PBS should properly resolve tmax deduction ms when adapters.generic.tmax-deduction-ms specified"() { + given: "PBS config with adapters.generic.tmax-deduction-ms" + def genericDeductionMs = PBSUtils.getRandomNumber(100, 300) + def randomTimeout = PBSUtils.getRandomNumber(MIN_AUCTION_BIDDER_TIMEOUT + genericDeductionMs as int, MAX_AUCTION_BIDDER_TIMEOUT as int) + def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": genericDeductionMs as String] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Default basic BidRequest with generic bidder" - def timeout = getRandomTimeout() def bidRequest = BidRequest.defaultBidRequest.tap { - tmax = timeout + tmax = randomTimeout } - when: "PBS processes auction request with warmup" - def bidResponse = prebidServerService.withWarmup().sendAuctionRequest(bidRequest) + when: "PBS processes auction request" + def bidResponse = prebidServerService.sendAuctionRequest(bidRequest) - then: "Bidder request should contain percent of request value" + then: "Bidder request should contain right value in tmax" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent,timeout)) + assert isInternalProcessingTime(bidderRequest.tmax, randomTimeout) - and: "PBS response should contain tmax from request" - assert bidResponse?.ext?.tmaxrequest == timeout as Long + and: "PBS response should contain tmax" + assert bidResponse?.ext?.tmaxrequest == randomTimeout as Long + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } private static long getPercentOfValue(int percent, int value) { (percent * value) / 100.0 as Long } - private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout){ + private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout) { 0 < requestTimeout - bidderRequestTimeout } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy index f94ae8772c9..ea2bbebb67e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TopicsHeaderSpec.groovy @@ -43,7 +43,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data[0].segment.id.sort().containsAll(firstSecBrowsingTopic.segments) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should populate headers with Observe-Browsing-Topics and emit warning when Sec-Browsing-Topics invalid header present in request"() { @@ -63,7 +63,7 @@ class TopicsHeaderSpec extends BaseSpec { assert !bidderRequest.user.data and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] and: "Response should contain Observe-Browsing-Topics header" assert response.responseBody.contains("\"warnings\":{\"prebid\":[{\"code\":999,\"message\":\"Invalid field " + @@ -94,7 +94,7 @@ class TopicsHeaderSpec extends BaseSpec { assert !bidderRequest.user.data and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] and: "Response should contain Observe-Browsing-Topics header" assert response.responseBody.contains("\"warnings\":{\"prebid\":[{\"code\":999,\"message\":\"Invalid field " + @@ -128,7 +128,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data.size() == 10 and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS shouldn't populate user.data when header Sec-Browsing-Topics contain 10 `p=` value and 11 valid"() { @@ -155,7 +155,7 @@ class TopicsHeaderSpec extends BaseSpec { assert response.responseBody.contains("Invalid field in Sec-Browsing-Topics header: ${header.replace(", ", "")} discarded due to limit reached.") and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should update user.data when Sec-Browsing-Topics header present in request"() { @@ -193,7 +193,7 @@ class TopicsHeaderSpec extends BaseSpec { secBrowsingTopic.segments].sort()) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should overlap segments when Sec-Browsing-Topics header present in request"() { @@ -229,7 +229,7 @@ class TopicsHeaderSpec extends BaseSpec { [randomSegment as String, firstSecBrowsingTopic.segments[0], secondSecBrowsingTopic.segments[0]] and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should multiple taxonomies when Sec-Browsing-Topics header present in request"() { @@ -255,7 +255,7 @@ class TopicsHeaderSpec extends BaseSpec { .containsAll([firstSecBrowsingTopic.segments[0], secondSecBrowsingTopic.segments[0]]) and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] } def "PBS should populate user.data with empty name when privacy sand box present with empty name"() { @@ -282,7 +282,7 @@ class TopicsHeaderSpec extends BaseSpec { assert bidderRequest.user.data[0].segment.id.sort() == secBrowsingTopic.segments.sort() and: "Response should contain Observe-Browsing-Topics header" - assert response.headers["Observe-Browsing-Topics"] == "?1" + assert response.headers["Observe-Browsing-Topics"] == ["?1"] where: topicsdomain << [null, ""] diff --git a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy index 45540134252..97a0015cea5 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/bidder/openx/OpenxSpec.groovy @@ -1,35 +1,105 @@ package org.prebid.server.functional.tests.bidder.openx +import org.prebid.server.functional.model.Currency 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.db.Account +import org.prebid.server.functional.model.request.auction.AuctionEnvironment import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.InterestGroupAuctionSupport +import org.prebid.server.functional.model.request.auction.PaaFormat +import org.prebid.server.functional.model.response.auction.InterestGroupAuctionBuyer +import org.prebid.server.functional.model.response.auction.InterestGroupAuctionBuyerExt +import org.prebid.server.functional.model.response.auction.InterestGroupAuctionIntent +import org.prebid.server.functional.model.response.auction.InterestGroupAuctionSeller import org.prebid.server.functional.model.response.auction.OpenxBidResponse import org.prebid.server.functional.model.response.auction.OpenxBidResponseExt +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils import spock.lang.Shared +import java.time.Instant + +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.OPENX_ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.DEVICE_ORCHESTRATED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.NOT_SUPPORTED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.SERVER_ORCHESTRATED +import static org.prebid.server.functional.model.request.auction.AuctionEnvironment.UNKNOWN +import static org.prebid.server.functional.model.request.auction.PaaFormat.IAB +import static org.prebid.server.functional.model.request.auction.PaaFormat.ORIGINAL +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer class OpenxSpec extends BaseSpec { private static final Map OPENX_CONFIG = ["adapters.openx.enabled" : "true", "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final Map OPENX_ALIAS_CONFIG = ["adapters.openx.aliases.openxalias.enabled" : "true", + "adapters.openx.aliases.openxalias.endpoint": "$networkServiceContainer.rootUri/auction".toString()] @Shared PrebidServerService pbsService = pbsServiceFactory.getService(OPENX_CONFIG) - def "PBS should populate fledge config when bid response with fledge and imp[0].ext.ae = 1"() { + @Override + def cleanupSpec() { + pbsServiceFactory.removeContainer(OPENX_CONFIG) + pbsServiceFactory.removeContainer(OPENX_CONFIG + OPENX_ALIAS_CONFIG) + } + + def "PBS should populate fledge config by default when bid response with fledge"() { given: "Default basic BidRequest with ae and openx bidder" def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.ae = 1 + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx } and: "Default bid response with fledge config" def impId = bidRequest.imp[0].id def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] - def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest).tap { + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain fledge config" + def auctionConfigs = response.ext?.prebid?.fledge?.auctionConfigs + assert auctionConfigs?.size() == 1 + assert auctionConfigs[0].impId == impId + assert auctionConfigs[0].bidder == bidResponse.seatbid[0].seat.value + assert auctionConfigs[0].adapter == bidResponse.seatbid[0].seat.value + assert auctionConfigs[0].config == fledgeConfig + + and: "PBS response shouldn't contain igb config" + assert !response.ext?.interestGroupAuctionIntent?.interestGroupAuctionBuyer + + and: "PBS response shouldn't contain igs config" + assert !response.ext?.interestGroupAuctionIntent?.interestGroupAuctionSeller + } + + def "PBS should populate fledge config when bid response with fledge and ext.prebid.paaFormat = ORIGINAL"() { + given: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = ORIGINAL + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { ext = new OpenxBidResponseExt().tap { fledgeAuctionConfigs = [(impId): fledgeConfig] } @@ -48,13 +118,61 @@ class OpenxSpec extends BaseSpec { assert auctionConfigs[0].bidder == bidResponse.seatbid[0].seat.value assert auctionConfigs[0].adapter == bidResponse.seatbid[0].seat.value assert auctionConfigs[0].config == fledgeConfig + + and: "PBS response shouldn't contain igb config" + assert !response.ext?.interestGroupAuctionIntent?.interestGroupAuctionBuyer + + and: "PBS response shouldn't contain igs config" + assert !response.ext?.interestGroupAuctionIntent?.interestGroupAuctionSeller } - def "PBS shouldn't populate fledge config when imp[0].ext.ae = 0"() { + def "PBS should take precedence request paa format over account value when both specified"() { + given: "Default bid request with openx" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = IAB + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account in the DB" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(paaformat: ORIGINAL)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response should contain igs config" + def interestGroupAuctionSeller = response.ext.interestGroupAuctionIntent[0].interestGroupAuctionSeller[0] + assert interestGroupAuctionSeller.impId == impId + assert interestGroupAuctionSeller.config == fledgeConfig + assert interestGroupAuctionSeller.ext.bidder == bidResponse.seatbid[0].seat.value + assert interestGroupAuctionSeller.ext.adapter == bidResponse.seatbid[0].seat.value + + and: "PBS response shouldn't contain igb config" + assert !response.ext?.interestGroupAuctionIntent?[0]?.interestGroupAuctionBuyer + } + + def "PBS shouldn't populate fledge config when bid response with fledge and ext.prebid.paaFormat = IAB"() { given: "Default basic BidRequest without ae" def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.ae = 0 + imp[0].ext.auctionEnvironment = NOT_SUPPORTED imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = IAB } and: "Default bid response" @@ -73,12 +191,21 @@ class OpenxSpec extends BaseSpec { then: "PBS response shouldn't contain fledge config" assert !response.ext.prebid.fledge + + and: "PBS response shouldn't contain igb config" + assert !response.ext?.interestGroupAuctionIntent?[0]?.interestGroupAuctionBuyer + + and: "PBS response should contain igs config" + def interestGroupAuctionSeller = response.ext.interestGroupAuctionIntent[0].interestGroupAuctionSeller[0] + assert interestGroupAuctionSeller.impId == impId + assert interestGroupAuctionSeller.config + assert interestGroupAuctionSeller.ext.bidder == bidResponse.seatbid[0].seat.value + assert interestGroupAuctionSeller.ext.adapter == bidResponse.seatbid[0].seat.value } - def "PBS shouldn't populate fledge config when imp[0].ext.ae = 1 and bid response didn't return fledge config"() { + def "PBS shouldn't populate fledge config when bid response didn't return fledge config"() { given: "Default basic BidRequest without ae" def bidRequest = BidRequest.defaultBidRequest.tap { - imp[0].ext.ae = 1 imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx } @@ -97,5 +224,358 @@ class OpenxSpec extends BaseSpec { then: "PBS response shouldn't contain fledge config" assert !response.ext.prebid.fledge + + and: "PBS response shouldn't contain igi config" + assert !response?.ext?.interestGroupAuctionIntent + } + + def "PBS should populate fledge and iab output config when bid response with fledge and paa formant IAB"() { + given: "Default bid request with openx" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = requestPaaFormant + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Save account in the DB" + def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(paaformat: accountPaaFormat)) + def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response should contain igs config" + def interestGroupAuctionSeller = response.ext.interestGroupAuctionIntent[0].interestGroupAuctionSeller[0] + assert interestGroupAuctionSeller.impId == impId + assert interestGroupAuctionSeller.config == fledgeConfig + assert interestGroupAuctionSeller.ext.bidder == bidResponse.seatbid[0].seat.value + assert interestGroupAuctionSeller.ext.adapter == bidResponse.seatbid[0].seat.value + + and: "PBS response shouldn't contain igb config" + assert !response.ext?.interestGroupAuctionIntent?[0]?.interestGroupAuctionBuyer + + where: + accountPaaFormat | requestPaaFormant + IAB | IAB + null | IAB + IAB | null + } + + def "PBS should populate fledge config by default when bid response with fledge and requested aliases"() { + given: "PBS config with alias config" + def pbsService = pbsServiceFactory.getService(OPENX_CONFIG + OPENX_ALIAS_CONFIG) + + and: "Default basic BidRequest with ae and bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.openxAlias = Openx.defaultOpenx + ext.prebid.aliases = [(OPENX_ALIAS.value): OPENX] + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX_ALIAS).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain fledge config" + def auctionConfigs = response.ext?.prebid?.fledge?.auctionConfigs + assert auctionConfigs?.size() == 1 + assert auctionConfigs[0].impId == impId + assert auctionConfigs[0].bidder == OPENX_ALIAS.value + assert auctionConfigs[0].adapter == OPENX_ALIAS.value + assert auctionConfigs[0].config == fledgeConfig + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent + } + + def "PBS should populate iab config when bid response with fledge and requested aliases"() { + given: "PBS config" + def pbsService = pbsServiceFactory.getService(OPENX_CONFIG + OPENX_ALIAS_CONFIG) + + and: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.openxAlias = Openx.defaultOpenx + ext.prebid.aliases = [(OPENX_ALIAS.value): OPENX] + ext.prebid.paaFormat = IAB + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX_ALIAS).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response should contain igs config" + def interestGroupAuctionSeller = response.ext.interestGroupAuctionIntent[0].interestGroupAuctionSeller[0] + assert interestGroupAuctionSeller.impId == impId + assert interestGroupAuctionSeller.config == fledgeConfig + assert interestGroupAuctionSeller.ext.bidder == OPENX_ALIAS.value + assert interestGroupAuctionSeller.ext.adapter == OPENX_ALIAS.value + + and: "Response should contain seat" + assert response.seatbid[0].seat == OPENX_ALIAS + + and: "Response should contain seat" + assert response.seatbid[0].bid[0].ext.prebid.meta.adapterCode == OPENX_ALIAS + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent?[0].interestGroupAuctionBuyer + } + + def "PBS should populate fledge config by default when bid response with fledge and imp mismatched"() { + given: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Default bid response with fledge config" + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(fledgeImpId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain fledge config" + def auctionConfigs = response.ext?.prebid?.fledge?.auctionConfigs + assert auctionConfigs?.size() == 1 + assert auctionConfigs[0].impId == fledgeImpId + assert auctionConfigs[0].bidder == bidResponse.seatbid[0].seat.value + assert auctionConfigs[0].adapter == bidResponse.seatbid[0].seat.value + assert auctionConfigs[0].config == fledgeConfig + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent + + where: + fledgeImpId << [PBSUtils.randomString, PBSUtils.randomNumber as String, WILDCARD.value] + } + + def "PBS should log error and not populated fledge impId when bidder respond with not empty config, but an empty impid"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = IAB + } + + and: "Default bid response with fledge config without imp" + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(""): fledgeConfig] as Map + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flush metrics" + flushMetrics(pbsService) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "ExtIgiIgs with absent impId from bidder: ${OPENX.value}") + + and: "Bid response should contain warning" + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == + ["ExtIgiIgs with absent impId from bidder: ${OPENX.value}" as String] + + and: "Alert.general metric should be updated" + def metrics = pbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS shouldn't populate fledge or igi config when bidder respond with igb"() { + given: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + } + + and: "Default bid response with igb config" + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + interestGroupAuctionIntent = [new InterestGroupAuctionIntent( + interestGroupAuctionBuyer: [new InterestGroupAuctionBuyer( + origin: PBSUtils.randomString, + maxBid: PBSUtils.randomDecimal, + cur: PBSUtils.getRandomEnum(Currency), + pbs: [(PBSUtils.randomString): PBSUtils.randomString], + ext: new InterestGroupAuctionBuyerExt( + bidder: PBSUtils.randomString, + adapter: PBSUtils.randomString + ) + )]) + ] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent + } + + def "PBS should throw error when requested unknown paa format"() { + given: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = PaaFormat.INVALID + } + + and: "Default bid response with fledge config" + def impId = bidRequest.imp[0].id + def fledgeConfig = [(PBSUtils.randomString): PBSUtils.randomString] + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + fledgeAuctionConfigs = [(impId): fledgeConfig] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.responseBody.startsWith("Invalid request format: Error decoding bidRequest: " + + "Cannot deserialize value of type `org.prebid.server.auction.model.PaaFormat` " + + "from String \"invalid\": not one of the values accepted for Enum class: [original, iab]") + } + + def "PBS shouldn't cause error when igs and igb empty array"() { + given: "Default basic BidRequest with ae and openx bidder" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.auctionEnvironment = DEVICE_ORCHESTRATED + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + ext.prebid.paaFormat = paaFormat + } + + and: "Default bid response with igs config" + def bidResponse = OpenxBidResponse.getDefaultBidResponse(bidRequest, OPENX).tap { + ext = new OpenxBidResponseExt().tap { + interestGroupAuctionIntent = [new InterestGroupAuctionIntent( + interestGroupAuctionSeller: interestGroupAuctionSeller, + interestGroupAuctionBuyer: interestGroupAuctionBuyer + )] + } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain fledge config" + assert !response.ext?.prebid?.fledge?.auctionConfigs + + and: "PBS response shouldn't contain igi config" + assert !response.ext?.interestGroupAuctionIntent + + where: + paaFormat | interestGroupAuctionSeller | interestGroupAuctionBuyer + IAB | [new InterestGroupAuctionSeller()] | [new InterestGroupAuctionBuyer()] + ORIGINAL | [] | [] + } + + def "PBS shouldn't change auction environment in imp.ext.igs and not emit a warning when it is present in both imp.ext and imp.ext.igs"() { + given: "Default bid request with populated imp.ext" + def extAuctionEnv = PBSUtils.getRandomEnum(AuctionEnvironment, [SERVER_ORCHESTRATED, UNKNOWN]) + def extIgsAuctionEnv = PBSUtils.getRandomEnum(AuctionEnvironment, [SERVER_ORCHESTRATED, UNKNOWN]) + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.tap { + auctionEnvironment = extAuctionEnv + interestGroupAuctionSupports = new InterestGroupAuctionSupport(auctionEnvironment: extIgsAuctionEnv) + } + } + + when: "PBS processes auction request" + def bidResponse = pbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should imp[].{ae/ext.igs.ae} same value as requested" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp[0].ext.auctionEnvironment == extAuctionEnv + assert bidderRequest.imp[0].ext.interestGroupAuctionSupports.auctionEnvironment == extIgsAuctionEnv + + and: "Response shouldn't contain errors" + assert !bidResponse.ext.errors + + and: "Response shouldn't contain warnings" + assert !bidResponse.ext.warnings } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy new file mode 100644 index 00000000000..1582f3b200d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy @@ -0,0 +1,1162 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.config.AbTest +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.Stage +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.FetchStatus +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.AnalyticResult +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION +import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.config.ModuleHookImplementation.RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +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.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.response.auction.InvocationStatus.INVOCATION_FAILURE +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.AB_TESTING +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.ORTB2_BLOCKING +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_INVOCATION + +class AbTestingModuleSpec extends ModuleBaseSpec { + + private final static String NO_INVOCATION_METRIC = "modules.module.%s.stage.%s.hook.%s.success.no-invocation" + private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + private final static String EXECUTION_ERROR_METRIC = "modules.module.%s.stage.%s.hook.%s.execution-error" + private final static Integer MIN_PERCENT_AB = 0 + private final static Integer MAX_PERCENT_AB = 100 + private final static String INVALID_HOOK_MESSAGE = "Hook implementation does not exist or disabled" + + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ModuleName.ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ModuleName.ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + + private final static Map MULTI_MODULE_CONFIG = getResponseCorrectionConfig() + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': null] + + private static final PrebidServerService ortbModulePbsService = pbsServiceFactory.getService(getOrtb2BlockingSettings()) + private static final PrebidServerService pbsServiceWithMultipleModules = pbsServiceFactory.getService(MULTI_MODULE_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(getOrtb2BlockingSettings()) + pbsServiceFactory.removeContainer(MULTI_MODULE_CONFIG) + } + + def "PBS shouldn't apply a/b test config when config of ab test is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def abTest = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + enabled = false + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + it.abTests = [abTest] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + } + + def "PBS shouldn't apply valid a/b test config when module is disabled"() { + given: "PBS service with disabled module config" + def pbsConfig = getOrtb2BlockingSettings(false) + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [INVOCATION_FAILURE, INVOCATION_FAILURE] + it.action == [null, null] + it.analyticsTags == [null, null] + it.message == [INVALID_HOOK_MESSAGE, INVALID_HOOK_MESSAGE] + } + + and: "Metric for specified module should be with error call" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config when module name is not matched"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(moduleName)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + moduleName << [ModuleName.ORTB2_BLOCKING.code.toUpperCase(), PBSUtils.randomString] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to allow modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN] + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for allowed to run ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to skip modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + and: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + } + + def "PBS should apply a/b test config for each module when multiple config are presents with different percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN] + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + } + + def "PBS should ignore accounts property for a/b test config when ab test config specialize for specific account"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + } + + def "PBS should apply a/b test config and run module when config is on max percentage or default value"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and run module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + MAX_PERCENT_AB | null + null | MAX_PERCENT_AB + null | null + } + + def "PBS should apply a/b test config and skip module when config is on min percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and skip this module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + MIN_PERCENT_AB | null + null | MIN_PERCENT_AB + } + + def "PBS shouldn't apply a/b test config without warnings and errors when percent config is out of lover range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + PBSUtils.randomNegativeNumber | null + null | PBSUtils.randomNegativeNumber + } + + def "PBS should apply a/b test config and run module without warnings and errors when percent config is out of appear range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS should apply ab test config for ortb module and run it" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + PBSUtils.getRandomNumber(MAX_PERCENT_AB) | null + null | PBSUtils.getRandomNumber(MAX_PERCENT_AB) + } + + def "PBS should include analytics tags when a/b test config when logAnalyticsTag is enabled or empty"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module without analytics tags" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + true | null + null | true + null | null + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + } + + and: "Shouldn't include any analytics tags" + assert !invocationResults?.analyticsTags?.any() + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is non-applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MAX_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't apply analytics tags for all module stages when module contain multiple stages"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB) + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for all stages of specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status.every { status -> status == it.status.first() } + it.action.every { action -> action == it.action.first() } + } + + and: "All resonances have same analytics" + def abTestingInvocationResults = (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name == AB_TESTING.value } + verifyAll(abTestingInvocationResults) { + it.status.flatten().every { status -> status == it.status.flatten().first() } + it.results.status.flatten().every { status -> status == it.results.status.flatten().first() } + it.results.values.module.flatten().every { module -> module == it.results.values.module.flatten().first() } + } + } + + def "PBS should apply a/b test config from host config when accounts is not specified when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, accouns).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + accouns << [null, []] + } + + def "PBS should apply a/b test config from host config for specific accounts and only specified module when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber, accountId]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should apply a/b test config from host config for specific account and general config when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, []).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code, [accountId]).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + and: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config from host config for non specified accounts when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten() == [ORTB2_BLOCKING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW] + it.analyticsTags.activities.results.values.module.flatten().every { it == null } + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be as default call" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from default account and only specified module when host and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def hostExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(hostExecutionPlan)] + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module and call it based on all execution plans" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION, NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING, AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other modules and call them based on all execution plans" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 2 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 2 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from account over default account and only specified module when specific account and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = new ExecutionPlan(abTests: [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)]) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + + and: "PBS should apply ab test config for specified module" + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED] + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + private static List filterInvocationResultsByModule(List invocationResults, ModuleName moduleName) { + invocationResults.findAll { it.hookId.moduleCode == moduleName.code } + } + + private static BidRequest getBidRequestWithTrace() { + defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy new file mode 100644 index 00000000000..23316766a7d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy @@ -0,0 +1,537 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AdminConfig +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingConfig +import org.prebid.server.functional.model.config.PbResponseCorrection +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.RichmediaFilter +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +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.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE +import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES +import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +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.request.auction.BidRequest.getDefaultBidRequest +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION + +class GeneralModuleSpec extends ModuleBaseSpec { + + private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + private final static String NOOP_METRIC = "modules.module.%s.stage.%s.hook.%s.success.noop" + + private final static Map DISABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'false'] + private final static Map ENABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'true'] + + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + private final static Map MULTI_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) + + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES))] + + private static final PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) + private static final PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(MULTI_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) + pbsServiceFactory.removeContainer(MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) + } + + def "PBS should call all modules and traces response when account config is empty and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING, PB_RICHMEDIA_FILTER].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModule) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | pbResponseCorrectionConfig + new RichmediaFilter() | new PbResponseCorrection() + new RichmediaFilter() | new PbResponseCorrection(enabled: false) + new RichmediaFilter() | new PbResponseCorrection(enabled: true) + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection() + new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) + } + + def "PBS should call all modules and traces response when default-account includes modules config and require-config-to-invoke is enabled"() { + given: "PBS service with module config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter(), ortb2Blocking: new Ortb2BlockingConfig()) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(modules: pbsModulesConfig) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, ortb2Blocking: ortb2BlockingConfig) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + + where: + pbRichmediaFilterConfig | ortb2BlockingConfig + new RichmediaFilter() | new Ortb2BlockingConfig() + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig() + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig(attributes: [:] as Map) + } + + def "PBS should call specified module and traces response when account config includes that module and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account with enabled response correction module" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter()))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called module" + def invocationTrace = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + } + + def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + def "PBS should call all modules without account config when modules enabled in module-execution host config"() { + given: "PBS service with module-execution config" + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'true'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call any module without account config when modules disabled in module-execution host config"() { + given: "PBS service with module-execution config" + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call module and not override host config when default-account module-execution config enabled module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should call module without account module config when default-account module-execution config enabling module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call any modules without account config when default-account module-execution config not enabling module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): moduleExecutionStatus])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + moduleExecutionStatus << [false, null] + } + + def "PBS should prioritize specific account module-execution config over default-account module-execution config when both are present"() { + given: "PBS service with default account config" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): false])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true]))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } +} 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..c0933a238e7 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 @@ -2,11 +2,23 @@ package org.prebid.server.functional.tests.module import org.prebid.server.functional.model.config.Endpoint import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Stage +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.InvocationResult import org.prebid.server.functional.tests.BaseSpec +import org.prebid.server.functional.util.PBSUtils +import static org.prebid.server.functional.model.ModuleName.OPTABLE_TARGETING +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.ModuleName.PB_REQUEST_CORRECTION +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE 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.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class ModuleBaseSpec extends BaseSpec { @@ -21,14 +33,21 @@ 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, [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + protected static Map getRichMediaFilterSettings(String scriptPattern, - boolean filterMraidEnabled = true, + Boolean filterMraidEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { ["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, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]]))] + .findAll { it.value != null } .collectEntries { key, value -> [(key.toString()): value.toString()] } } @@ -39,4 +58,39 @@ class ModuleBaseSpec extends BaseSpec { "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid" : filterMraidEnabled] .collectEntries { key, value -> [(key.toString()): value.toString()] } } + + protected static Map getOptableTargetingSettings(boolean isEnabled = true, Endpoint endpoint = OPENRTB2_AUCTION) { + ["hooks.${OPTABLE_TARGETING.code}.enabled": isEnabled as String, + "hooks.modules.${OPTABLE_TARGETING.code}.api-endpoint" : "$networkServiceContainer.rootUri/stored-cache".toString(), + "hooks.modules.${OPTABLE_TARGETING.code}.tenant" : PBSUtils.randomString, + "hooks.modules.${OPTABLE_TARGETING.code}.origin" : PBSUtils.randomString, + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(PROCESSED_AUCTION_REQUEST): [OPTABLE_TARGETING]]))] + .collectEntries { key, value -> [(key.toString()): value.toString()] } + } + + protected static Map getOrtb2BlockingSettings(boolean isEnabled = true) { + ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String] + } + + protected static Map getRequestCorrectionSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) { + ["hooks.${PB_REQUEST_CORRECTION.code}.enabled": "true", + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_REQUEST_CORRECTION, [stage]))] + } + + protected static Map getRulesEngineSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) { + ["hooks.${PB_RULE_ENGINE.code}.enabled" : "true", + "hooks.${PB_RULE_ENGINE.code}.rule-cache.expire-after-minutes" : "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-cache.max-size" : "20000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-initial-delay-millis": "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-max-delay-millis" : "10000", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-exponential-factor" : "1.2", + "hooks.${PB_RULE_ENGINE.code}.rule-parsing.retry-exponential-jitter" : "1.2", + "hooks.host-execution-plan" : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RULE_ENGINE, [stage]))] + } + + protected static List getAnalyticResults(BidResponse response) { + response.ext.prebid.modules?.trace?.stages?.first() + ?.outcomes?.first()?.groups?.first() + ?.invocationResults?.first()?.analyticsTags?.activities + } } 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..82355a47996 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/analyticstag/AnalyticsTagsModuleSpec.groovy @@ -0,0 +1,300 @@ +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 static final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(getOrtb2BlockingSettings()) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(getOrtb2BlockingSettings()) + } + + 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/optabletargeting/CacheStorageSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy new file mode 100644 index 00000000000..9a71d42ba7f --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/optabletargeting/CacheStorageSpec.groovy @@ -0,0 +1,207 @@ +package org.prebid.server.functional.tests.module.optabletargeting + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.IdentifierType +import org.prebid.server.functional.model.config.OperatingSystem +import org.prebid.server.functional.model.config.OptableTargetingConfig +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.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.PublicCountryIp +import org.prebid.server.functional.model.request.auction.User +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.StoredCache +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils + +import static org.apache.commons.codec.binary.Base64.encodeBase64 +import static org.mockserver.model.HttpStatusCode.NOT_FOUND_404 +import static org.prebid.server.functional.model.ModuleName.OPTABLE_TARGETING +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class CacheStorageSpec extends ModuleBaseSpec { + + private static final String METRIC_CREATIVE_SIZE_TEXT = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.entry_size.text" + private static final String METRIC_CREATIVE_TTL_TEXT = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.entry_ttl.text" + + private static final String METRIC_CREATIVE_READ_OK = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.read.ok" + private static final String METRIC_CREATIVE_READ_ERR = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.read.err" + private static final String METRIC_CREATIVE_WRITE_OK = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.write.ok" + private static final String METRIC_CREATIVE_WRITE_ERR = "prebid_cache.module_storage.${OPTABLE_TARGETING.code}.write.err" + + private static final StoredCache storedCache = new StoredCache(networkServiceContainer) + + private static final Map CACHE_STORAGE_CONFIG = ['storage.pbc.path' : "$networkServiceContainer.rootUri/stored-cache".toString(), + 'storage.pbc.call-timeout-ms': '1000', + 'storage.pbc.enabled' : 'true', + 'cache.module.enabled' : 'true', + 'pbc.api.key' : PBSUtils.randomString, + 'cache.api-key-secured' : 'false'] + private static final Map MODULE_STORAGE_CACHE_CONFIG = getOptableTargetingSettings() + CACHE_STORAGE_CONFIG + private static final PrebidServerService prebidServerStoredCacheService = pbsServiceFactory.getService(MODULE_STORAGE_CACHE_CONFIG) + + def setup() { + storedCache.reset() + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(MODULE_STORAGE_CACHE_CONFIG) + } + + def "PBS should update error metrics when no cached requests present"() { + given: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for new saved text storage cache" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_READ_ERR] == 1 + + and: "No updates for success metrics" + assert !metrics[METRIC_CREATIVE_SIZE_TEXT] + assert !metrics[METRIC_CREATIVE_TTL_TEXT] + assert !metrics[METRIC_CREATIVE_READ_OK] + } + + def "PBS should update error metrics when external service responded with invalid values"() { + given: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + storedCache.setTargetingResponse(bidRequest, targetingConfig) + storedCache.setCachingResponse(NOT_FOUND_404) + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update error metrics" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_WRITE_ERR] == 1 + + and: "No updates for success metrics" + assert !metrics[METRIC_CREATIVE_WRITE_OK] + } + + def "PBS should update metrics for new saved text storage cache when no cached requests"() { + given: "Current value of metric prebid cache" + def okInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_WRITE_OK) + + and: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + def targetingResult = storedCache.setTargetingResponse(bidRequest, targetingConfig) + storedCache.setCachingResponse() + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for new saved text storage cache" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_SIZE_TEXT] == new String(encodeBase64(encode(targetingResult).bytes)).size() + assert metrics[METRIC_CREATIVE_WRITE_OK] == okInitialValue + 1 + + and: "PBS should include histogram metric" + assert metrics[METRIC_CREATIVE_TTL_TEXT] + } + + def "PBS should update metrics for stored cached requests cache when proper record present"() { + given: "Current value of metric prebid cache" + def textInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_SIZE_TEXT) + def ttlInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_TTL_TEXT) + def writeInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_WRITE_OK) + def readErrorInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_READ_ERR) + def writeErrorInitialValue = getCurrentMetricValue(prebidServerStoredCacheService, METRIC_CREATIVE_WRITE_ERR) + + and: "Default BidRequest with cache and device info" + def randomIfa = PBSUtils.randomString + def system = PBSUtils.getRandomEnum(OperatingSystem) + def bidRequest = getBidRequestForModuleCacheStorage(randomIfa, system) + + and: "Account with optable targeting module" + def targetingConfig = OptableTargetingConfig.getDefault([(IdentifierType.fromOS(system)): randomIfa]) + def account = createAccountWithRequestCorrectionConfig(bidRequest, targetingConfig) + accountDao.save(account) + + and: "Mocked external request" + storedCache.setCachedTargetingResponse(bidRequest) + storedCache.setCachingResponse() + + and: "Flash metrics" + flushMetrics(prebidServerStoredCacheService) + + when: "PBS processes auction request" + prebidServerStoredCacheService.sendAuctionRequest(bidRequest) + + then: "PBS should update metrics for stored cached requests" + def metrics = prebidServerStoredCacheService.sendCollectedMetricsRequest() + assert metrics[METRIC_CREATIVE_READ_OK] == 1 + + and: "No updates for new saved text storage metrics" + assert metrics[METRIC_CREATIVE_SIZE_TEXT] == textInitialValue + assert metrics[METRIC_CREATIVE_TTL_TEXT] == ttlInitialValue + assert metrics[METRIC_CREATIVE_WRITE_OK] == writeInitialValue + + and: "No update for error metrics" + assert metrics[METRIC_CREATIVE_READ_ERR] == readErrorInitialValue + assert metrics[METRIC_CREATIVE_WRITE_ERR] == writeErrorInitialValue + } + + private static BidRequest getBidRequestForModuleCacheStorage(String ifa, OperatingSystem os) { + BidRequest.defaultBidRequest.tap { + it.enableCache() + it.user = new User(id: PBSUtils.randomString, data: [Data.defaultData], eids: [Eid.defaultEid]) + it.device = new Device(geo: Geo.FPDGeo, + ip: PBSUtils.getRandomEnum(PublicCountryIp.class).v4, + ifa: ifa, + ua: PBSUtils.randomString, + os: os) + } + } + + private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest, + OptableTargetingConfig optableTargetingConfig) { + + def pbsModulesConfig = new PbsModulesConfig(optableTargeting: optableTargetingConfig) + def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: bidRequest.accountId, config: accountConfig) + } +} 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..a7a97bc8816 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy @@ -0,0 +1,1647 @@ +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.AccountAuctionConfig +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.BidderControls +import org.prebid.server.functional.model.request.auction.GenericPreferredBidder +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 String WILDCARD = '*' + private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true", + "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + private static final Map PBS_CONFIG = getOrtb2BlockingSettings() + IX_CONFIG + + ['adapter-defaults.ortb.multiformat-supported': 'false'] + + private static final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(PBS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } + + 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 be able to send original battr ortb2 attribute when bid request imps type doesn't match with attribute type"() { + given: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], 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, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain single media type" + assert bidderRequest.imp.first.mediaTypes.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: + bidRequest | attributeName + BidRequest.defaultVideoRequest | BANNER_BATTR + BidRequest.defaultAudioRequest | VIDEO_BATTR + BidRequest.defaultBidRequest | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: preferredMediaType)) + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], 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, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + 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: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + + def "PBS shouldn't be able to send original battr ortb2 attribute when account level preferredMediaType doesn't match with attribute type"() { + given: "Default bid request with multiply types" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp.first.banner = Banner.defaultBanner + imp.first.video = Video.defaultVideo + imp.first.audio = Audio.defaultAudio + } + + and: "Account in the DB with blocking configuration" + def ortb2Attribute = PBSUtils.randomNumber + def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName).tap { + config.auction = new AccountAuctionConfig(preferredMediaType: [(GENERIC): preferredMediaType]) + } + accountDao.save(account) + + and: "Default bidder response with ortb2 attributes" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)] + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes the auction request" + def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest) + + then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest?.imp?.first?.banner?.battr + assert !bidderRequest?.imp?.first?.video?.battr + assert !bidderRequest?.imp?.first?.audio?.battr + + and: "PBS request should contain only preferred media type" + assert bidderRequest.imp.first.mediaTypes == [preferredMediaType] + + 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: + preferredMediaType | attributeName + VIDEO | BANNER_BATTR + AUDIO | VIDEO_BATTR + BANNER | AUDIO_BATTR + } + + 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 == GENERIC + 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/pbrequestcorrection/PbRequestCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy new file mode 100644 index 00000000000..1f6bbcaf13d --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy @@ -0,0 +1,458 @@ +package org.prebid.server.functional.tests.module.pbrequestcorrection + +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.PbRequestCorrectionConfig +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.AppExt +import org.prebid.server.functional.model.request.auction.AppPrebid +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.auction.OperationState +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.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.OperationState.YES + +class PbRequestCorrectionSpec extends ModuleBaseSpec { + + private static final String PREBID_MOBILE = "prebid-mobile" + private static final String DEVICE_PREBID_MOBILE_PATTERN = "PrebidMobile/" + private static final String ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.1.5") + private static final String ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.2.3") + private static final String ANDROID = "android" + private static final String IOS = "IOS" + + private static final PrebidServerService pbsServiceWithRequestCorrectionModule = pbsServiceFactory.getService(getRequestCorrectionSettings()) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(getRequestCorrectionSettings()) + } + + def "PBS should remove positive instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl.every { it == null } + + where: + imps | bundle | requestCorrectionConfig + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial + [Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }] | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true) + } + + def "PBS shouldn't remove negative instl from imps for android app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp = imps + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + imps << [[Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = OperationState.NO }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = null }], + [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = OperationState.NO }]] + } + + def "PBS shouldn't remove positive instl from imps for not android or not prebid-mobile app when request correction is enabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(source), version: PBSUtils.getRandomVersion(ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(bundle) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source + IOS | PREBID_MOBILE + PBSUtils.randomString | PREBID_MOBILE + ANDROID | PBSUtils.randomString + ANDROID | PBSUtils.randomString + PREBID_MOBILE + ANDROID | PREBID_MOBILE + PBSUtils.randomString + } + + def "PBS shouldn't remove positive instl from imps for app when request correction is enabled for account but some required parameter is empty"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: source, version: version) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = instl + app.bundle = bundle + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + bundle | source | version | instl + null | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | null | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES + ANDROID | PREBID_MOBILE | null | YES + ANDROID | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is threshold"() { + given: "Android APP bid request with version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.2.3") + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is higher then threshold"() { + given: "Android APP bid request with version higher then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.2.4")) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is disabled for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithInterstitial(interstitialCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + + where: + enabled | interstitialCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove positive instl from imps for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + imp.first.instl = YES + app.bundle = PBSUtils.getRandomCase(ANDROID) + app.ext = new AppExt(prebid: prebid) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain original imp.instl" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.instl == bidRequest.imp.instl + } + + def "PBS should remove pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request shouldn't contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.device.ua + + where: + deviceUa | requestCorrectionConfig + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true) + } + + def "PBS should remove only pattern device.ua when request correction is enabled for account and user agent correction enabled"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua.contains(deviceUa.replaceAll("PrebidMobile/[0-9][^ ]*", '').trim()) + + where: + deviceUa << ["${PBSUtils.randomNumber} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}", + "${PBSUtils.randomString} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString} ${PBSUtils.randomString}", + "${DEVICE_PREBID_MOBILE_PATTERN}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}", + "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}" + ] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and user agent correction disabled"() { + given: "Android APP bid request with version lover then version threshold" + def deviceUserAgent = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUserAgent) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithUserAgentCorrection(userAgentCorrectionEnabled, enabled) + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUserAgent + + where: + enabled | userAgentCorrectionEnabled + false | true + null | true + true | false + true | null + null | null + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and source not a prebid-mobile"() { + given: "Android APP bid request with version lover then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: source, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + + where: + source << ["prebid", + "mobile", + PREBID_MOBILE + PBSUtils.randomString, + PBSUtils.randomString + PREBID_MOBILE, + "mobile-prebid", + PBSUtils.randomString] + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version biggest that threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version threshold" + def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.1.6")) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern when request correction is enabled for account and version threshold"() { + given: "Android APP bid request with version higher then version threshold" + def randomDeviceUa = PBSUtils.randomString + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6"))) + device = new Device(ua: randomDeviceUa) + } + + and: "Account in the DB" + def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection + def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain device.ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == randomDeviceUa + } + + def "PBS shouldn't remove device.ua pattern from device for android app when request correction is not applied for account"() { + given: "Android APP bid request with version lover then version threshold" + def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD) + def deviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}" + def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { + app.ext = new AppExt(prebid: prebid) + device = new Device(ua: deviceUa) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig())) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain request device ua" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.device.ua == deviceUa + } + + private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest, + PbRequestCorrectionConfig requestCorrectionConfig) { + def pbsModulesConfig = new PbsModulesConfig(pbRequestCorrection: requestCorrectionConfig) + def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig) + def accountConfig = new AccountConfig(hooks: accountHooksConfig) + new Account(uuid: bidRequest.accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy new file mode 100644 index 00000000000..4477e6d9d38 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineAliasSpec.groovy @@ -0,0 +1,262 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.request.auction.Imp + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.OPENX_ALIAS +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineAliasSpec extends RuleEngineBaseSpec { + + def "PBS should leave only hard alias bidder at imps when hard alias bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidders = [OPENX, AMX, OPENX_ALIAS, GENERIC] + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, bidders)) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(OPENX_ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.size() == 1 + + and: "Bid response should contain seatBid.seat" + assert bidResponse.seatbid.seat == [OPENX_ALIAS] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == MULTI_BID_ADAPTERS.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 3 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == MULTI_BID_ADAPTERS.sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove hard alias bidder from imps when hard alias bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, OPENX_ALIAS, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(OPENX_ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == [bidRequest.imp[1].id] + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX_ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[1].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should leave only soft alias bidder at imps when soft alias bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [ALIAS, AMX, OPENX])) + ext.prebid.aliases = [(ALIAS.value): GENERIC] + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seat" + assert bidResponse.seatbid.seat == [ALIAS] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved.sort() == MULTI_BID_ADAPTERS.sort() + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 3 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == MULTI_BID_ADAPTERS.sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.unique().flatten() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove soft alias bidder from imps when soft alias bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [ALIAS, AMX, OPENX])) + ext.prebid.aliases = [(ALIAS.value): GENERIC] + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(ALIAS)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def impResult = result.results[0] + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + impResult.status == SUCCESS + impResult.values.analyticsKey == groups.analyticsKey + impResult.values.modelVersion == groups.version + impResult.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + impResult.values.resultFunction == groups.rules.first.results.first.function.value + impResult.values.conditionFired == groups.rules.first.conditions.first + impResult.values.biddersRemoved == [ALIAS] + impResult.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + impResult.appliedTo.impIds == [bidRequest.imp[1].id] + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == ALIAS + assert seatNonBid.nonBid[0].impId == bidRequest.imp[1].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy new file mode 100644 index 00000000000..d27d44e0fc0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineBaseSpec.groovy @@ -0,0 +1,196 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.bidder.BidderName +import org.prebid.server.functional.model.bidder.Generic +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.PbRulesEngine +import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.request.auction.Amx +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.DistributionChannel +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.ImpUnitCode +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.tests.module.ModuleBaseSpec +import org.prebid.server.functional.util.PBSUtils +import spock.lang.Retry + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.ALIAS +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.OPENX_ALIAS +import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.pricefloors.Country.USA +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.ImpUnitCode.GPID +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.PB_AD_SLOT +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.STORED_REQUEST +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.TAG_ID +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID + +@Retry //TODO remove in 3.34+ +abstract class RuleEngineBaseSpec extends ModuleBaseSpec { + + protected static final List MULTI_BID_ADAPTERS = [GENERIC, OPENX, AMX].sort() + protected static final String APPLIED_FOR_ALL_IMPS = "*" + protected static final String DEFAULT_CONDITIONS = "default" + protected final static String CALL_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.call" + protected final static String NOOP_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.success.noop" + protected final static String UPDATE_METRIC = "modules.module.${PB_RULE_ENGINE.code}.stage.${PROCESSED_AUCTION_REQUEST.metricValue}.hook.${PB_RULES_ENGINE_PROCESSED_AUCTION_REQUEST.code}.success.update" + protected final static Closure INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an array of strings" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_SINGLE_STRING_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be a string" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_SINGLE_INTEGER_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an integer" + } + + protected final static Closure INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING = { accountId, functionType -> + "Failed to parse rule-engine config for account $accountId: " + + "Function '$functionType' configuration is invalid: " + + "Field '$functionType.fieldName' is required and has to be an array of integers" + } + + protected static final Map ENABLED_DEBUG_LOG_MODE = ["logging.level.root": "debug"] + protected static final Map OPENX_CONFIG = ["adapters.${OPENX}.enabled" : "true", + "adapters.${OPENX}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final Map AMX_CONFIG = ["adapters.${AMX}.enabled" : "true", + "adapters.${AMX}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final Map OPENX_ALIAS_CONFIG = ["adapters.${OPENX}.aliases.${OPENX_ALIAS}.enabled" : "true", + "adapters.${OPENX}.aliases.${OPENX_ALIAS}.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + protected static final String CONFIG_DATA_CENTER = PBSUtils.randomString + private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final Map GENERIC_CONFIG = [ + "adapters.${GENERIC.value}.usersync.redirect.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.redirect.support-cors": false as String, + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String] + protected static final PrebidServerService pbsServiceWithRulesEngineModule = pbsServiceFactory.getService(GENERIC_CONFIG + + getRulesEngineSettings() + AMX_CONFIG + OPENX_CONFIG + OPENX_ALIAS_CONFIG + ['datacenter-region': CONFIG_DATA_CENTER] + + ENABLED_DEBUG_LOG_MODE) + + protected static BidRequest getDefaultBidRequestWithMultiplyBidders(DistributionChannel distributionChannel = SITE) { + BidRequest.getDefaultBidRequest(distributionChannel).tap { + it.tmax = 5_000 // prevents timeout issues on slow pipelines + it.imp[0].ext.prebid.bidder.amx = new Amx() + it.imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + it.imp[0].ext.prebid.bidder.generic = new Generic() + it.ext.prebid.trace = VERBOSE + it.ext.prebid.returnAllBidStatus = true + } + } + + protected static Imp updateBidderImp(Imp imp, List bidders = MULTI_BID_ADAPTERS) { + imp.ext.prebid.bidder.tap { + openx = bidders.contains(OPENX) ? Openx.defaultOpenx : null + openxAlias = bidders.contains(OPENX_ALIAS) ? Openx.defaultOpenx : null + amx = bidders.contains(AMX) ? new Amx() : null + generic = bidders.contains(GENERIC) ? new Generic() : null + alias = bidders.contains(ALIAS) ? new Generic() : null + } + imp + } + + protected static void updateBidRequestWithGeoCountry(BidRequest bidRequest, Country country = USA) { + bidRequest.device = new Device(geo: new Geo(country: country)) + } + + protected static Account getAccountWithRulesEngine(String accountId, PbRulesEngine ruleEngine) { + def accountHooksConfiguration = new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRuleEngine: ruleEngine)) + new Account(uuid: accountId, config: new AccountConfig(hooks: accountHooksConfiguration)) + } + + protected static BidRequest createBidRequestWithDomains(DistributionChannel type, String domain, boolean usePublisher = true) { + def request = getDefaultBidRequestWithMultiplyBidders(type) + + switch (type) { + case SITE: + if (usePublisher) request.site.publisher.domain = domain + else request.site.domain = domain + break + case APP: + if (usePublisher) request.app.publisher.domain = domain + else request.app.domain = domain + break + case DOOH: + if (usePublisher) request.dooh.publisher.domain = domain + else request.dooh.domain = domain + break + } + request + } + + protected static BidRequest updatePublisherDomain(BidRequest bidRequest, DistributionChannel distributionChannel, String domain) { + switch (distributionChannel) { + case SITE: + bidRequest.site.publisher.domain = domain + break + case APP: + bidRequest.app.publisher.domain = domain + break + case DOOH: + bidRequest.dooh.publisher.domain = domain + break + } + bidRequest + } + + protected static String getImpAdUnitCodeByCode(Imp imp, ImpUnitCode code) { + switch (code) { + case TAG_ID: + return imp.tagId + case GPID: + return imp.ext.gpid + case PB_AD_SLOT: + return imp.ext.data.pbAdSlot + case STORED_REQUEST: + return imp.ext.prebid.storedRequest.id + default: + return null + } + } + + protected static String getImpAdUnitCode(Imp imp) { + [imp?.ext?.gpid, + imp?.tagId, + imp?.ext?.data?.pbAdSlot, + imp?.ext?.prebid?.storedRequest?.id] + .findResult { it } + } + + protected static waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) { + PBSUtils.waitUntil({ + pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + pbsServiceWithRulesEngineModule.isContainLogsByValue("Successfully parsed rule-engine config for account $bidRequest.accountId") + }) + } + + protected static waitUntilFailedParsedAndCacheAccount(bidRequest) { + PBSUtils.waitUntil({ + pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + pbsServiceWithRulesEngineModule.isContainLogsByValue("Failed to parse rule-engine config for account $bidRequest.accountId") + }) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy new file mode 100644 index 00000000000..5bea040218e --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineContextSpec.groovy @@ -0,0 +1,1146 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.ChannelType +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.db.StoredImp +import org.prebid.server.functional.model.pricefloors.MediaType +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.ImpExtContextData +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest +import org.prebid.server.functional.model.request.auction.Publisher +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ChannelType.WEB +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.CHANNEL +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.MEDIA_TYPE_IN +import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER +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.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.GPID +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.PB_AD_SLOT +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.STORED_REQUEST +import static org.prebid.server.functional.model.request.auction.ImpUnitCode.TAG_ID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineContextSpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when channel match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: CHANNEL)] + rules[0].conditions = [WEB.value] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when channel not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: CHANNEL)] + rules[0].conditions = [PBSUtils.getRandomEnum(ChannelType, [WEB]).value] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when domain match with condition"() { + given: "Default bid request with multiply bidder" + def randomDomain = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(distributionChannel).tap { + updatePublisherDomain(it, distributionChannel, randomDomain) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DOMAIN)] + rules[0].conditions = [randomDomain] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + distributionChannel << DistributionChannel.values() + } + + def "PBS shouldn't exclude bidder when domain not match with condition"() { + given: "Default bid request with random domain" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(distributionCahannel).tap { + updatePublisherDomain(it, distributionCahannel, PBSUtils.randomString) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DOMAIN)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + distributionCahannel << DistributionChannel.values() + } + + def "PBS should reject processing the rule engine when the domainIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiplyB bidders" + def bidRequest = bidRequestWithDomaint + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomNumber]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DOMAIN_IN)) + + where: + bidRequestWithDomaint << [ + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.domain = PBSUtils.randomString + }] + } + + def "PBS should exclude bidder when domainIn match with condition"() { + given: "Default bid request with multiply bidder" + def randomDomain = PBSUtils.randomString + def bidRequest = createBidRequestWithDomains(type, randomDomain, usePublisher) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomString, randomDomain]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + type | usePublisher + SITE | true + SITE | false + APP | true + APP | false + DOOH | true + DOOH | false + } + + def "PBS shouldn't exclude bidder when domainIn not match with condition"() { + given: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DOMAIN_IN + it.args = new RuleEngineFunctionArgs(domains: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(SITE).tap { + it.site.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + it.app.domain = PBSUtils.randomString + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.publisher = new Publisher(id: PBSUtils.randomString, domain: PBSUtils.randomString) + }, + getDefaultBidRequestWithMultiplyBidders(DOOH).tap { + it.dooh.domain = PBSUtils.randomString + }] + } + + def "PBS should exclude bidder when bundle match with condition"() { + given: "Default bid request with multiply bidder" + def bundle = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = bundle + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: BUNDLE)] + rules[0].conditions = [bundle] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when bundle not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: BUNDLE)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the bundleIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [PBSUtils.randomNumber]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, BUNDLE_IN)) + } + + def "PBS should exclude bidder when bundleIn match with condition"() { + given: "Default bid request with multiply bidders" + def bundle = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = bundle + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [bundle]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when bundleIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.bundle = PBSUtils.randomString + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = BUNDLE_IN + it.args = new RuleEngineFunctionArgs(bundles: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the mediaTypeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [mediaTypeInArgs]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + then: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, MEDIA_TYPE_IN)) + + where: + mediaTypeInArgs << [null, PBSUtils.randomNumber] + } + + def "PBS should exclude bidder when mediaTypeIn match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Setup bidder response" + def bidderResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidderResponse) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [BANNER.value]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when mediaTypeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = MEDIA_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomEnum(MediaType, [BANNER])]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when adUnitCode match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].ext.gpid = gpid + imp[0].tagId = tagId + imp[0].ext.data = new ImpExtContextData(pbAdSlot: pbAdSlot) + imp[0].ext.prebid.storedRequest = prebidStoredRequest + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: AD_UNIT_CODE)] + rules[0].conditions = [getImpAdUnitCode(bidRequest.imp[0])] + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gpid | tagId | pbAdSlot | prebidStoredRequest + PBSUtils.getRandomString() | null | null | null + null | PBSUtils.getRandomString() | null | null + null | null | PBSUtils.getRandomString() | null + null | null | null | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + PBSUtils.getRandomString() | PBSUtils.getRandomString() | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | PBSUtils.getRandomString() | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | null | PBSUtils.getRandomString() | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + null | null | null | new PrebidStoredRequest(id: PBSUtils.getRandomString()) + } + + def "PBS shouldn't exclude bidder when adUnitCode not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: AD_UNIT_CODE)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the adUnitCodeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].tagId = PBSUtils.randomString + imp[0].ext.gpid = PBSUtils.randomString + imp[0].ext.data = new ImpExtContextData(pbAdSlot: PBSUtils.randomString) + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest).tap { + impData = Imp.getDefaultImpression() + } + storedImpDao.save(storedImp) + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [arguments]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Account cache" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, AD_UNIT_CODE_IN)) + + where: + arguments << [PBSUtils.randomBoolean, PBSUtils.randomNumber] + } + + def "PBS should exclude bidder when adUnitCodeIn match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + imp[0].tagId = PBSUtils.randomString + imp[0].ext.gpid = PBSUtils.randomString + imp[0].ext.data = new ImpExtContextData(pbAdSlot: PBSUtils.randomString) + imp[0].ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomString) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [PBSUtils.randomString, + getImpAdUnitCodeByCode(bidRequest.imp[0], impUnitCode)]) + } + } + + and: "Save storedImp into DB" + def storedImp = StoredImp.getStoredImp(bidRequest) + storedImpDao.save(storedImp) + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + impUnitCode << [TAG_ID, GPID, PB_AD_SLOT, STORED_REQUEST] + } + + def "PBS shouldn't exclude bidder when adUnitCodeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = AD_UNIT_CODE_IN + it.args = new RuleEngineFunctionArgs(codes: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy new file mode 100644 index 00000000000..8e9de1807c9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineCoreSpec.groovy @@ -0,0 +1,870 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineModelDefault +import org.prebid.server.functional.model.config.RuleEngineModelDefaultArgs +import org.prebid.server.functional.model.config.RuleSet +import org.prebid.server.functional.model.config.RulesEngineModelGroup +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.UNKNOWN +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.ResultFunction.LOG_A_TAG +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithLogATagResult +import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_NO_BID +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineCoreSpec extends RuleEngineBaseSpec { + + def "PBS should remove bidder and not update analytics when bidder matched with conditions and without analytics key"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets without analytics value" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].analyticsKey = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metric" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBs should populate call and update metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC] == 1 + assert metrics[UPDATE_METRIC] == 1 + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should remove bidder from imps and use default 203 value for seatNonBid when seatNonBid null and exclude bidder in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression)) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + it.ruleSets[0].modelGroups[0].rules[0].results[0].args.seatNonBid = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid.impId.sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove bidder from imps and not update seatNonBid when returnAllBidStatus disabled and exclude bidder in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression)) + updateBidRequestWithGeoCountry(it) + ext.prebid.tap { + returnAllBidStatus = false + trace = VERBOSE + } + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + it.ruleSets[0].modelGroups[0].rules[0].results[0].args.seatNonBid = ERROR_NO_BID + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Response shouldn't populate seatNon bid with code 203" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == ERROR_NO_BID + it.appliedTo.impIds == bidRequest.imp.id + } + } + + def "PBS shouldn't include unknown bidder when unknown bidder specified in result account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(UNKNOWN)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.size() == 0 + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS shouldn't exclude unknown bidder when unknown bidder specified in result account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(UNKNOWN)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS should include one bidder and update analytics when multiple bidders specified and one included in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(OPENX)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat == [OPENX] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [GENERIC, AMX].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [GENERIC, AMX].sort() + assert seatNonBid.nonBid.impId.flatten() == [bidRequest.imp[0].id, bidRequest.imp[0].id] + assert seatNonBid.nonBid.statusCode.flatten() == + [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should remove bidder by device geo from imps when bidder excluded in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(Imp.defaultImpression) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seatBids" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid.impId.sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE, + REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should leave only include bidder at imps when bidder include in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat == [GENERIC] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == [AMX, OPENX] + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should populate seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [AMX, OPENX].sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS should only logATag when present only function log a tag"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + it.imp.add(updateBidderImp(Imp.defaultImpression, [OPENX, AMX])) + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithLogATagResult()] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + def impResult = result.results[0] + verifyAll(impResult) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + + it.appliedTo.impIds == [APPLIED_FOR_ALL_IMPS] + } + + verifyAll(impResult) { + !it.values.biddersRemoved + !it.values.seatNonBid + } + } + + def "PBS should remove bidder and update analytics when first rule sets disabled and second enabled in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets.first.enabled = false + it.ruleSets.add(RuleSet.createRuleSets()) + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[1].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should skip rule set and take next one when rule sets not a processed auction request"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules engine and several rule sets" + def firstResults = [createRuleEngineModelRuleWithExcludeResult(GENERIC), + createRuleEngineModelRuleWithExcludeResult(AMX), + createRuleEngineModelRuleWithExcludeResult(OPENX)] + def secondResult = [createRuleEngineModelRuleWithExcludeResult(AMX)] + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = firstResults + it.ruleSets[0].stage = stage as Stage + it.ruleSets.add(RuleSet.createRuleSets()) + it.ruleSets[1].modelGroups[0].rules[0].results = secondResult + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, OPENX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[1].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == AMX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + stage << Stage.values() - PROCESSED_AUCTION_REQUEST + } + + def "PBS should take rule with higher weight and remove bidder when two model group with different weight"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with few model group" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + it.weight = 1 + it.rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC)] + } + it.ruleSets[0].modelGroups.add(RulesEngineModelGroup.createRulesModuleGroup()) + it.ruleSets[0].modelGroups[1].tap { + it.weight = 100 + it.rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC)] + } + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[1] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't log the default model group and should modify response when other rule fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with default model" + def analyticsValue = PBSUtils.randomString + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: analyticsValue))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS should log the default model group and shouldn't modify response when other rules not fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it, BULGARIA) + } + + and: "Account with default model" + def analyticsValue = PBSUtils.randomString + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: analyticsValue))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + def impResult = result.results[0] + verifyAll(impResult) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == analyticsValue + it.values.resultFunction == LOG_A_TAG.value + it.values.conditionFired == DEFAULT_CONDITIONS + it.appliedTo.impIds == [APPLIED_FOR_ALL_IMPS] + } + + and: "Analytics imp result shouldn't contain remove info" + verifyAll(impResult) { + !it.values.biddersRemoved + !it.values.seatNonBid + } + } + + def "PBS shouldn't log the default model group and modify response when rules fire"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with default model" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = [new RuleEngineModelDefault( + function: LOG_A_TAG, + args: new RuleEngineModelDefaultArgs(analyticsValue: PBSUtils.randomString))] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain two seat" + assert bidResponse.seatbid.size() == 2 + + and: "Response should contain seat bid" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result should contain detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy new file mode 100644 index 00000000000..229e54a0ff0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineDeviceSpec.groovy @@ -0,0 +1,435 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.request.auction.Device +import org.prebid.server.functional.model.request.auction.DeviceType +import org.prebid.server.functional.util.PBSUtils +import spock.lang.RepeatUntilFailure + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE_IN +import static org.prebid.server.functional.model.pricefloors.Country.USA +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineDeviceSpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when deviceCountry match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_COUNTRY)] + rules[0].conditions = [USA.toString()] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceCountry not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema[0].function = DEVICE_COUNTRY + rules[0].conditions = [PBSUtils.getRandomEnum(Country, [USA]).toString()] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when deviceType match with condition"() { + given: "Default bid request with multiply bidders" + def deviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: deviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_TYPE)] + rules[0].conditions = [deviceType.value as String] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceType not match with condition"() { + given: "Default bid request with multiply bidders" + def requestDeviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: requestDeviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DEVICE_TYPE)] + rules[0].conditions = [PBSUtils.getRandomEnum(DeviceType, [requestDeviceType]).value as String] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should reject processing the rule engine when the deviceCountryIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_COUNTRY_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.randomString]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DEVICE_COUNTRY_IN)) + } + + def "PBS should reject processing the rule engine when the deviceTypeIn schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomString()]) + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING(bidRequest.accountId, DEVICE_TYPE_IN)) + } + + def "PBS should exclude bidder when deviceTypeIn match with condition"() { + given: "Default bid request with multiply bidders" + def deviceType = PBSUtils.getRandomEnum(DeviceType) + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: deviceType) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [deviceType.value]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when deviceTypeIn not match with condition"() { + given: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + device = new Device(devicetype: PBSUtils.getRandomEnum(DeviceType)) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DEVICE_TYPE_IN + it.args = new RuleEngineFunctionArgs(types: [PBSUtils.getRandomEnum(DeviceType).value as String]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy new file mode 100644 index 00000000000..f710ba39f93 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineInfrastructureSpec.groovy @@ -0,0 +1,264 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.DATA_CENTER +import static org.prebid.server.functional.model.config.RuleEngineFunction.DATA_CENTER_IN +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineInfrastructureSpec extends RuleEngineBaseSpec { + + def "PBS should reject processing rule engine when dataCenterIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(countries: [CONFIG_DATA_CENTER]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, DATA_CENTER_IN)) + } + + def "PBS should exclude bidder when dataCenterIn match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(datacenters: [CONFIG_DATA_CENTER]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when dataCentersIn not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = DATA_CENTER_IN + it.args = new RuleEngineFunctionArgs(datacenters: [PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when dataCenter match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DATA_CENTER)] + rules[0].conditions = [CONFIG_DATA_CENTER] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when dataCenter not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: DATA_CENTER)] + rules[0].conditions = [PBSUtils.randomString] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy new file mode 100644 index 00000000000..c4f207544b5 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEnginePrivacySpec.groovy @@ -0,0 +1,915 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.model.config.RuleEngineModelSchema +import org.prebid.server.functional.model.request.GppSectionId +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.Content +import org.prebid.server.functional.model.request.auction.Data +import org.prebid.server.functional.model.request.auction.Eid +import org.prebid.server.functional.model.request.auction.Regs +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.util.PBSUtils +import org.prebid.server.functional.util.privacy.TcfConsent + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.FPD_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_IN +import static org.prebid.server.functional.model.config.RuleEngineFunction.TCF_IN_SCOPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.USER_FPD_AVAILABLE +import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE +import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS + +class RuleEnginePrivacySpec extends RuleEngineBaseSpec { + + def "PBS should exclude bidder when eidAvailable match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [Eid.getDefaultEid()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: EID_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when eidAvailable not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: eids) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema[0].function = EID_AVAILABLE + rules[0].conditions = ["TRUE"] + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + eids << [null, []] + } + + def "PBS should reject processing rule engine when eidIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomNumber]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_STRINGS_LOG_WARNING(bidRequest.accountId, EID_IN)) + } + + def "PBS should exclude bidder when eidIn match with condition"() { + given: "Bid request with multiply bidders" + def eid = Eid.getDefaultEid() + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [eid]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomString, eid.source, PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when eidIn not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(eids: [Eid.getDefaultEid()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = EID_IN + it.args = new RuleEngineFunctionArgs(sources: [PBSUtils.randomString, PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should exclude bidder when userFpdAvailable match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = requestedUfpUser + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: USER_FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + requestedUfpUser << [new User(data: [Data.defaultData], ext: new UserExt(data: UserExtData.FPDUserExtData)), + new User(ext: new UserExt(data: UserExtData.FPDUserExtData)), + new User(data: [Data.defaultData])] + } + + def "PBS shouldn't exclude bidder when userFpdAvailable not match with condition"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + user = requestedUfpUser + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: USER_FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + requestedUfpUser << [new User(data: null), new User(data: [null]), + new User(ext: new UserExt(data: null)), + new User(data: null, ext: new UserExt(data: null)) + ] + } + + def "PBS should exclude bidder when fpdAvailable match with condition"() { + given: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: FPD_AVAILABLE)] + } + + and: "Account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(ext: new UserExt(data: UserExtData.FPDUserExtData)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.content = new Content(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.ext = new SiteExt(data: SiteExtData.FPDSiteExtData) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.content = new Content(data: [Data.defaultData]) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.ext = new AppExt(data: new AppExtData(language: PBSUtils.randomString)) + } + ] + } + + def "PBS shouldn't exclude bidder when fpdAvailable not match with condition"() { + given: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: FPD_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + bidRequest << [ + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(data: null, ext: new UserExt(data: null)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + user = new User(ext: new UserExt(data: null)) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.content = new Content(data: [null]) + site.ext = new SiteExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders().tap { + site.ext = new SiteExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.content = new Content(data: [null]) + app.ext = new AppExt(data: null) + }, + getDefaultBidRequestWithMultiplyBidders(APP).tap { + app.ext = new AppExt(data: null) + } + ] + } + + def "PBS should exclude bidder when gppSidAvailable match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [PBSUtils.getRandomEnum(GppSectionId).getIntValue()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: GPP_SID_AVAILABLE)] + } + + and: "Account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when gppSidAvailable not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: gppSid) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema = [new RuleEngineModelSchema(function: GPP_SID_AVAILABLE)] + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gppSid << [[PBSUtils.randomNegativeNumber], null] + } + + def "PBS should reject processing rule engine when gppSidIn schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: 0, gppSid: [PBSUtils.getRandomEnum(GppSectionId, [GppSectionId.TCF_EU_V2]).getIntValue()]) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [PBSUtils.randomString]) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_INTEGERS_LOG_WARNING(bidRequest.accountId, GPP_SID_IN)) + } + + def "PBS should exclude bidder when gppSidIn match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [gppSectionId.getIntValue()]) + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [gppSectionId]) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == groups.rules.first.results.first.args.bidders + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + } + + def "PBS shouldn't exclude bidder when gppSidIn not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gppSid: [gppSectionId.getIntValue()]) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = GPP_SID_IN + it.args = new RuleEngineFunctionArgs(sids: [PBSUtils.getRandomEnum(GppSectionId, [gppSectionId]).getIntValue()]) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gppSectionId << GppSectionId.values() - GppSectionId.TCF_EU_V2 + } + + def "PBS should exclude bidder when tcfInScope match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: gdpr) + user = new User(ext: new UserExt(consent: new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build())) + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: TCF_IN_SCOPE)] + rules[0].conditions = [condition] + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved == groups.rules.first.results.first.args.bidders + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + + where: + gdpr | condition + 1 | 'true' + 0 | 'false' + } + + def "PBS shouldn't exclude bidder when tcfInScope not match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + regs = new Regs(gdpr: gdpr) + user = new User(ext: new UserExt(consent: new TcfConsent.Builder() + .setPurposesLITransparency(BASIC_ADS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build())) + } + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + schema = [new RuleEngineModelSchema(function: TCF_IN_SCOPE)] + rules[0].conditions = [condition] + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + gdpr | condition + 0 | 'true' + 1 | 'false' + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy new file mode 100644 index 00000000000..cce9631e1b9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSpecialSpec.groovy @@ -0,0 +1,321 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.PERCENT +import static org.prebid.server.functional.model.config.RuleEngineFunction.PREBID_KEY +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineSpecialSpec extends RuleEngineBaseSpec { + + def "PBS should reject processing rule engine when percent schema function args contain invalid data"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: PBSUtils.randomString) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result should contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_SINGLE_INTEGER_LOG_WARNING(bidRequest.accountId, PERCENT)) + } + + def "PBS should exclude bidder when percent match with condition"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: PBSUtils.getRandomNumber(100)) + } + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when percent less than zero"() { + given: "Default bid request with multiply bidder" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PERCENT + it.args = new RuleEngineFunctionArgs(percent: percent) + } + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + where: + percent << [0, PBSUtils.randomNegativeNumber] + } + + def "PBS should reject processing the rule engine when the prebidKey schema function contains incompatible arguments"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs(key: PBSUtils.randomNumber) + } + } + + and: "Account with rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBS response should not contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, INVALID_CONFIGURATION_FOR_SINGLE_STRING_LOG_WARNING(bidRequest.accountId, PREBID_KEY)) + } + + def "PBS should exclude bidder when prebidKey match with condition"() { + given: "Default bid request with multiply bidder" + def keyField = "key" + def keyString = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + ext.prebid.keyValuePairs = [(keyString): keyField] + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs((keyField): keyString) + } + it.ruleSets[0].modelGroups[0].rules[0].conditions = [keyField] + } + + and: "Save account with rule engine config" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [GENERIC, AMX].sort() + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == OPENX + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't exclude bidder when prebidKey not match with condition"() { + given: "Default bid request with multiply bidder" + def key = PBSUtils.randomString + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + ext.prebid.keyValuePairs = [(key): PBSUtils.randomString] + } + + and: "Create rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = PREBID_KEY + it.args = new RuleEngineFunctionArgs(key: key) + } + it.ruleSets[0].modelGroups[0].rules[0].conditions = [PBSUtils.randomString] + } + + and: "Save account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy new file mode 100644 index 00000000000..881a8211ca4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineSyncSpec.groovy @@ -0,0 +1,298 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.util.HttpUtil + +import static org.prebid.server.functional.model.ModuleName.PB_RULE_ENGINE +import static org.prebid.server.functional.model.bidder.BidderName.AMX +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.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithExcludeResult +import static org.prebid.server.functional.model.config.RuleEngineModelRuleResult.createRuleEngineModelRuleWithIncludeResult +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + +class RuleEngineSyncSpec extends RuleEngineBaseSpec { + + def "PBS should remove bidder from imps when bidder has ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == [OPENX, AMX] + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about module exclude" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == groups.rules.first.results.first.args.bidders.sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should contain seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 1 + def seatNonBid = bidResponse.ext.seatnonbid[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + } + + def "PBS shouldn't remove bidder from imps when bidder has ID in the uids cookie and bidder excluded and ifSyncedId=false in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, false)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS shouldn't remove bidder from imps when bidder hasn't ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithExcludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS should remove requested bidders at imps when bidder has ID in the uids cookie and bidder include and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response shouldn't contain seat" + assert bidResponse.seatbid.seat == [GENERIC] + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [AMX, OPENX].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response should contain seatNon bid with code 203" + assert bidResponse.ext.seatnonbid.size() == 2 + def seatNonBid = bidResponse.ext.seatnonbid + assert seatNonBid.seat.sort() == [OPENX, AMX].sort() + assert seatNonBid.nonBid.impId.flatten().unique().sort() == bidRequest.imp.id.sort() + assert seatNonBid.nonBid.statusCode.flatten().unique() == [REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE] + } + + def "PBS shouldn't include bidder at imps when bidder has ID in the uids cookie and bidder include and ifSyncedId=false in account config"() { + given: "Bid request with multiply imps bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, false)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cookies headers" + def cookieHeader = HttpUtil.getCookieHeader(UidsCookie.defaultUidsCookie) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest, cookieHeader) + + then: "Bid response shouldn't contain seat" + assert !bidResponse.seatbid.seat + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Analytics result should contain info about name and status" + def analyticsResult = getAnalyticResults(bidResponse) + def result = analyticsResult[0] + assert result.name == PB_RULE_ENGINE.code + assert result.status == SUCCESS + + and: "Analytics result detail info" + def groups = pbRuleEngine.ruleSets[0].modelGroups[0] + verifyAll(result.results[0]) { + it.status == SUCCESS + it.values.analyticsKey == groups.analyticsKey + it.values.modelVersion == groups.version + it.values.analyticsValue == groups.rules.first.results.first.args.analyticsValue + it.values.resultFunction == groups.rules.first.results.first.function.value + it.values.conditionFired == groups.rules.first.conditions.first + it.values.biddersRemoved.sort() == [OPENX, AMX, GENERIC].sort() + it.values.seatNonBid == REQUEST_BIDDER_REMOVED_BY_RULE_ENGINE_MODULE + it.appliedTo.impIds == bidRequest.imp.id + } + + and: "Response shouldn't contain seatNon bid with code 203" + assert !bidResponse.ext.seatnonbid + } + + def "PBS should leave request bidder at imps when bidder hasn't ID in the uids cookie and bidder excluded and ifSyncedId=true in account config"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].rules[0].results = [createRuleEngineModelRuleWithIncludeResult(GENERIC, true)] + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy new file mode 100644 index 00000000000..dbf7395c1f0 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbruleengine/RuleEngineValidationSpec.groovy @@ -0,0 +1,437 @@ +package org.prebid.server.functional.tests.module.pbruleengine + +import org.prebid.server.functional.model.config.RuleEngineFunctionArgs +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.config.PbRulesEngine.createRulesEngineWithRule +import static org.prebid.server.functional.model.config.RuleEngineFunction.AD_UNIT_CODE +import static org.prebid.server.functional.model.config.RuleEngineFunction.BUNDLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.CHANNEL +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_COUNTRY +import static org.prebid.server.functional.model.config.RuleEngineFunction.DEVICE_TYPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.DOMAIN +import static org.prebid.server.functional.model.config.RuleEngineFunction.EID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.FPD_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.GPP_SID_AVAILABLE +import static org.prebid.server.functional.model.config.RuleEngineFunction.TCF_IN_SCOPE +import static org.prebid.server.functional.model.config.RuleEngineFunction.USER_FPD_AVAILABLE +import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA + +class RuleEngineValidationSpec extends RuleEngineBaseSpec { + + def "PBS shouldn't remove bidder when rule engine not fully configured in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRulesEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBs should populate call and noop metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC] == 1 + assert metrics[NOOP_METRIC] == 1 + + and: "PBs should populate update metrics" + assert !metrics[UPDATE_METRIC] + + where: + pbRulesEngine << [ + createRulesEngineWithRule().tap { it.ruleSets = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].stage = null }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].schema = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules = [] }, + createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].results = [] } + ] + } + + def "PBS shouldn't remove bidder when rule engine not fully configured in account without rule conditions"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRuleEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].conditions = [] } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBs should populate noop metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[NOOP_METRIC] == 1 + } + + def "PBS shouldn't remove bidder and emit a warning when args rule engine not fully configured in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRuleEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups[0].rules[0].results[0].args = null } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + and: "Flush metrics" + flushMetrics(pbsServiceWithRulesEngineModule) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should populate failer metrics" + def metrics = pbsServiceWithRulesEngineModule.sendCollectedMetricsRequest() + assert metrics[NOOP_METRIC] == 1 + } + + def "PBS shouldn't remove bidder and emit a warning when model group rule engine not fully configured in account"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with enabled rules engine" + def pbRulesEngine = createRulesEngineWithRule().tap { it.ruleSets[0].modelGroups = [] } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.getAccountId(), pbRulesEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should emit failed logs" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Failed to parse rule-engine config for account $bidRequest.accountId:" + + " Weighted list cannot be empty") + } + + def "PBS shouldn't log default model when rule does not fired and empty model default"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it, BULGARIA) + } + + and: "Account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].modelDefault = null + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + } + + def "PBS shouldn't remove bidder when rule engine disabled or absent in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with disabled or without rules engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + where: + pbRuleEngine << [createRulesEngineWithRule(false), null] + } + + def "PBS shouldn't remove bidder when rule sets disabled in account"() { + given: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with disabled rules sets" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets.first.enabled = false + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilSuccessfullyParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + } + + def "PBS shouldn't remove any bidder without cache account request"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with rules sets" + def pbRuleEngine = createRulesEngineWithRule() + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBs should emit failed logs" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Parsing rule for account $bidRequest.accountId").size() == 1 + } + + def "PBS shouldn't take rule with higher weight and not remove bidder when weight negative or zero"() { + given: "Start up time" + def start = Instant.now() + + and: "Bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders().tap { + updateBidRequestWithGeoCountry(it) + } + + and: "Account with few model group" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].tap { + it.weight = weight + } + } + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "Bid response should contain seats" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "PBs should perform bidder requests" + assert bidder.getBidderRequests(bidRequest.id) + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "Analytics result shouldn't contain info about module exclude" + assert !getAnalyticResults(bidResponse) + + and: "PBS should emit log" + def logsByTime = pbsServiceWithRulesEngineModule.getLogsByTime(start) + assert getLogsByText(logsByTime, "Failed to parse rule-engine config for account $bidRequest.accountId:" + + " Weight must be greater than zero") + + where: + weight << [PBSUtils.randomNegativeNumber, 0] + } + + def "PBS should reject processing rule engine when #function schema function contain args"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default bid request with multiply bidders" + def bidRequest = getDefaultBidRequestWithMultiplyBidders() + + and: "Create account with rule engine config" + def pbRuleEngine = createRulesEngineWithRule().tap { + it.ruleSets[0].modelGroups[0].schema[0].tap { + it.function = function + it.args = RuleEngineFunctionArgs.defaultFunctionArgs + } + } + + and: "Save account with rule engine" + def accountWithRulesEngine = getAccountWithRulesEngine(bidRequest.accountId, pbRuleEngine) + accountDao.save(accountWithRulesEngine) + + and: "Cache account" + waitUntilFailedParsedAndCacheAccount(bidRequest) + + when: "PBS processes auction request" + def bidResponse = pbsServiceWithRulesEngineModule.sendAuctionRequest(bidRequest) + + then: "PBs should perform bidder request" + assert bidder.getBidderRequests(bidRequest.id) + + and: "Bid response should contain all requested bidders" + assert bidResponse.seatbid.seat.sort() == MULTI_BID_ADAPTERS + + and: "Analytics result shouldn't contain info about rule engine" + assert !getAnalyticResults(bidResponse) + + and: "PBS response shouldn't contain seatNonBid" + assert !bidResponse.ext.seatnonbid + + and: "PBS should not contain errors, warnings" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "Logs should contain error" + def logs = pbsServiceWithRulesEngineModule.getLogsByTime(startTime) + assert getLogsByText(logs, "Failed to parse rule-engine config for account ${bidRequest.accountId}: " + + "Function '${function.value}' configuration is invalid: No arguments allowed") + + where: + function << [DEVICE_TYPE, AD_UNIT_CODE, BUNDLE, DOMAIN, TCF_IN_SCOPE, GPP_SID_AVAILABLE, FPD_AVAILABLE, + USER_FPD_AVAILABLE, EID_AVAILABLE, CHANNEL, DEVICE_COUNTRY] + } +} 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..2426a3fd316 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/responsecorrenction/ResponseCorrectionSpec.groovy @@ -0,0 +1,642 @@ +package org.prebid.server.functional.tests.module.responsecorrenction + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountCacheConfig +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 static int OPTIMAL_MAX_LENGTH = 20 + private static final Map PBS_CONFIG = ["adapter-defaults.modifying-vast-xml-allowed": "false", + "adapters.generic.modifying-vast-xml-allowed": "false"] + + getResponseCorrectionConfig() + + private static final PrebidServerService pbsServiceWithResponseCorrectionModule = pbsServiceFactory.getService(PBS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } + + 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?.first?.bid?.first?.ext?.prebid?.meta?.mediaType + + 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?.first?.bid?.first?.ext?.prebid?.meta?.mediaType + + 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 + } + + def "PBS should modify response when requested video impression respond with invalid adm VAST keyword and disabled cache config"() { + 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).tap { + config.auction = new AccountAuctionConfig(cache: new AccountCacheConfig(enabled: false)) + } + 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)}" + ] + } + + 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..517da393668 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,25 +12,88 @@ 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 { 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 pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME, false)) + private static final Map DISABLED_FILTER_SPECIFIC_PATTERN_NAME_CONFIG = getRichMediaFilterSettings(PATTERN_NAME, false) + private static final Map SPECIFIC_PATTERN_NAME_CONFIG = getRichMediaFilterSettings(PATTERN_NAME) + private static final Map SNAKE_SPECIFIC_PATTERN_NAME_CONFIG = (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 static final PrebidServerService pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(DISABLED_FILTER_SPECIFIC_PATTERN_NAME_CONFIG) + private static final PrebidServerService pbsServiceWithEnabledMediaFilter = pbsServiceFactory.getService(SPECIFIC_PATTERN_NAME_CONFIG) + private static final PrebidServerService pbsServiceWithEnabledMediaFilterAndDifferentCaseStrategy = pbsServiceFactory.getService(SNAKE_SPECIFIC_PATTERN_NAME_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(DISABLED_FILTER_SPECIFIC_PATTERN_NAME_CONFIG) + pbsServiceFactory.removeContainer(SPECIFIC_PATTERN_NAME_CONFIG) + pbsServiceFactory.removeContainer(SNAKE_SPECIFIC_PATTERN_NAME_CONFIG) + } + + def "PBS should process request without rich media module when host config have empty settings"() { + given: "Prebid server with empty settings for module" + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + 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)] + } + + and: "Stored bid response in DB" + def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid[0].bid[0].adm = PBSUtils.randomString + } + 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 = prebidServerService.sendAuctionRequest(bidRequest) + + then: "Response header should contain seatbid" + assert response.seatbid.size() == 1 + + and: "Response shouldn't contain errors of invalid creation" + assert !response.ext.errors + + and: "Response shouldn't contain analytics" + assert !getAnalyticResults(response) + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + pbsConfig << [getRichMediaFilterSettings(PBSUtils.randomString, null), + getRichMediaFilterSettings(null, true), + getRichMediaFilterSettings(null, false), + getRichMediaFilterSettings(null, null)] + } 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 +129,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 +152,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 + 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 +173,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 +211,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 +236,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 + 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 +257,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 +295,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 +320,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 + 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 +338,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 +376,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 +410,47 @@ class RichMediaFilterSpec extends ModuleBaseSpec { admValue << [PATTERN_NAME, PATTERN_NAME_ACCOUNT] } - private static List getAnalyticResults(BidResponse response) { - response.ext.prebid.modules?.trace?.stages?.first() - ?.outcomes?.first()?.groups?.first() - ?.invocationResults?.first()?.analyticStags?.activities + 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 + 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}"] } } 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/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy new file mode 100644 index 00000000000..c04e4d263d7 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -0,0 +1,1240 @@ +package org.prebid.server.functional.tests.pricefloors + +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.db.Account +import org.prebid.server.functional.model.pricefloors.PriceFloorField +import org.prebid.server.functional.model.pricefloors.Rule +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule +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.ExtPrebidFloors +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.MultiBid +import org.prebid.server.functional.model.request.auction.Native +import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +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.MediaType +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD +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.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { + + private static final Integer MIN_ADJUST_VALUE = 0 + private static final Integer MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) + private static final Integer MAX_CPM_ADJUST_VALUE = 5 + private static final Integer MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final String WILDCARD = '*' + + private static final Map PBS_CONFIG = PbsConfig.currencyConverterConfig + + FLOORS_CONFIG + + GENERIC_ALIAS_CONFIG + + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + + ["adapter-defaults.ortb.multiformat-supported": "true"] + private static PrebidServerService pbsService + + def setupSpec() { + pbsService = pbsServiceFactory.getService(PBS_CONFIG) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } + + def "PBS should reverse imp.floors for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomDecimal(impPrice) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should left original bidderRequest with null floors when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when request with multiple imps has specific bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.first.id }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.last.id }.price == [bidResponse.seatbid.first.bid.last.price] + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [getReverseAdjustedPrice(firstImpPrice, ruleValue as BigDecimal, adjustmentType), secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder with specific dealId when request with multiple imps has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when account config has bidAdjustments"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize exact imp.floors reverser for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomDecimal + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, exactRulePrice, STATIC)] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: firstRuleValue, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: secondRuleValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [applyReverseAdjustments(impPrice, [firstRule, secondRule])] + + where: + firstRuleType | secondRuleType | firstRuleValue | secondRuleValue + MULTIPLIER | CPM | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + MULTIPLIER | STATIC | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + MULTIPLIER | MULTIPLIER | PBSUtils.randomPrice | PBSUtils.randomPrice + CPM | CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) + CPM | STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + CPM | MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + STATIC | MULTIPLIER | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + } + + def "PBS should prioritize revert with lower resulting value for matching bidder when request has multiple media types"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def impPrice = PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + def currency = USD + def firstRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: firstRulePrice, currency: currency)]]) + def secondRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: secondRulePrice, currency: currency)]]) + def bidRequest = getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM).tap { + cur = [currency] + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + imp[0].ext.prebid.bidder.generic = null + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + imp.first.banner = Banner.getDefaultBanner() + imp.first.nativeObj = Native.getDefaultNative() + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(primaryType): firstRule, (BANNER): secondRule]) + ext.prebid.multibid = [new MultiBid(bidder: OPENX, maxBids: 3)] + } + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomDecimal() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid = Bid.getDefaultMultiTypesBids(bidRequest.imp.first) { + price = originalPrice + ext = new BidExt() + } + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to first matched rule" + getMediaTypedBids(response, BidMediaType.from(primaryType)).price == [getAdjustedPrice(originalPrice, firstRulePrice, MULTIPLIER)] + getMediaTypedBids(response, BidMediaType.BANNER).price == [getAdjustedPrice(originalPrice, secondRulePrice, MULTIPLIER)] + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain revert imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, [firstRulePrice, secondRulePrice].max(), MULTIPLIER)] + + where: + primaryType | firstRulePrice | secondRulePrice + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE), currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur, currencyRatesResponse) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == CurrencyUtil.getPriceAfterCurrencyConversion(adjustedBidPrice, bidResponse.cur, currency, currencyRatesResponse) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def convertedReverseAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + def reversedAdjustBidPrice = getReverseAdjustedPrice(impPrice, convertedReverseAdjustment, adjustmentRule.adjustmentType) + assert bidderRequest.imp.bidFloor == [reversedAdjustBidPrice] + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomDecimal, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with USD currency" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == + CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should apply bidAdjustments revert for imp.floors after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def reversedBidPrice = impPrice / bidAdjustmentFactorsPrice + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(reversedBidPrice, adjustmentRule.value, adjustmentType)] + + where: + adjustmentType | impPrice | adjustmentValue + MULTIPLIER | PBSUtils.getRandomPrice() | PBSUtils.getRandomPrice() + CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has invalid value bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + assert pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = impPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + pbsService.isContainLogsByValue(errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, adjustmentPrice, MULTIPLIER)] + + where: + adjustmentType << [CPM, STATIC] + } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + getBidRequestWithFloors(MediaType.VIDEO).tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } + } + + private static BigDecimal getReverseAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice / adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice + adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return PBSUtils.roundDecimal(originalPrice, FLOOR_VALUE_PRECISION) + default: + return adjustedValue + } + } + + private static BigDecimal applyReverseAdjustments(BigDecimal originalPrice, List rules) { + if (!rules || rules.any { it.adjustmentType == STATIC }) { + return originalPrice + } + def result = originalPrice + rules.reverseEach { + result = getReverseAdjustedPrice(result, it.value, it.adjustmentType) + } + result + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } + } + + private static BidRequest getBidRequestWithFloors(MediaType type, + DistributionChannel channel = SITE) { + def floors = ExtPrebidFloors.extPrebidFloors.tap { + data.modelGroups.first.values = [(new Rule(channel: PBSUtils.randomString) + .getRule([PriceFloorField.CHANNEL])): PBSUtils.randomFloorValue] + } + BidRequest.getDefaultBidRequest(type, channel).tap { + ext.prebid.floors = floors + } + } +} 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..95ef1318637 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,6 @@ 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 @@ -14,16 +14,18 @@ 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.DistributionChannel import org.prebid.server.functional.model.request.auction.ExtPrebidFloors +import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Prebid import org.prebid.server.functional.model.request.auction.Video -import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.testcontainers.scaffolding.FloorsProvider import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode +import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer @@ -32,20 +34,39 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { public static final BigDecimal FLOOR_MIN = 0.5 public static final BigDecimal FLOOR_MAX = 2 - public static final Map FLOORS_CONFIG = ["price-floors.enabled" : "true", - "settings.default-account-config": encode(defaultAccountConfigSettings)] + public static final Map FLOORS_CONFIG = ["price-floors.enabled": "true"] - protected static final String basicFetchUrl = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer) + + protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT protected static final int MAX_MODEL_WEIGHT = 100 + protected static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } + + protected static final Closure URL_EMPTY_ERROR = { url -> "Failed to fetch price floor from provider for fetch.url '${url}'" + } + protected static final String FETCHING_DISABLED_ERROR = "Fetching is disabled" + protected static final Closure PRICE_FLOORS_ERROR_LOG = { bidRequest, reason, warningMessage -> + "Price Floors can't be resolved for account ${bidRequest.accountId} and request ${bidRequest.id}, reason: ${PRICE_FLOORS_WARNING_MESSAGE(reason, warningMessage)}" + } + protected static final Closure WARNING_MESSAGE = { message -> + "Price floors processing failed: parsing of request price floors is failed: $message" + } + protected static final Closure FETCHING_FLOORS_ERROR_LOG = { bidRequest, warningMessage -> + "Price floor fetching failed for account ${bidRequest.accountId}: ${URL_EMPTY_ERROR("$BASIC_FETCH_URL${bidRequest.accountId}")}, with a reason: $warningMessage" + } + private static final Closure PRICE_FLOORS_WARNING_MESSAGE = { reason, details -> + "Price floors processing failed: $reason. Following parsing of request price floors is failed: $details" + } + protected static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + + protected static final int FLOOR_VALUE_PRECISION = 4 private static final int DEFAULT_MODEL_WEIGHT = 1 - 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() { + currencyConversion.setCurrencyConversionRatesResponse() floorsProvider.setResponse() } @@ -55,19 +76,22 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { maxRules: 0, maxFileSizeKb: 200, maxAgeSec: 86400, - periodSec: 3600) + periodSec: 3600, + maxSchemaDims: 5) def floors = new AccountPriceFloorsConfig(enabled: true, fetch: fetch, enforceFloorsRate: 100, enforceDealFloors: true, adjustForBidAdjustment: true, - useDynamicData: true) + useDynamicData: true, + maxRules: 0, + maxSchemaDims: 3) new AccountConfig(auction: new AccountAuctionConfig(priceFloors: floors)) } protected static Account getAccountWithEnabledFetch(String accountId) { def priceFloors = new AccountPriceFloorsConfig(enabled: true, - fetch: new PriceFloorsFetch(url: basicFetchUrl + accountId, enabled: true)) + fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + accountId, enabled: true)) def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(priceFloors: priceFloors)) new Account(uuid: accountId, config: accountConfig) } @@ -84,7 +108,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { static BidRequest getStoredRequestWithFloors(DistributionChannel channel = SITE) { channel == SITE ? BidRequest.defaultStoredRequest.tap { ext.prebid.floors = ExtPrebidFloors.extPrebidFloors } - : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: 1, floors: ExtPrebidFloors.extPrebidFloors))) + : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: ENABLED, floors: ExtPrebidFloors.extPrebidFloors))) } @@ -96,33 +120,33 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { PBSUtils.getRandomNumber(DEFAULT_MODEL_WEIGHT, MAX_MODEL_WEIGHT) } - static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { - def adjustedValue = floorValue / bidAdjustment - PBSUtils.roundDecimal(adjustedValue, FLOOR_VALUE_PRECISION) - } - static BidRequest getBidRequestWithMultipleMediaTypes() { 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, + FetchStatus fetchStatus = INPROGRESS) { + PBSUtils.waitUntil({ getRequests(pbsService.sendAuctionRequest(bidRequest))[bidderName.value]?.first?.ext?.prebid?.floors?.fetchStatus != fetchStatus }, 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) } @@ -130,12 +154,4 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected BigDecimal getRoundedFloorValue(BigDecimal floorValue) { floorValue.setScale(FLOOR_VALUE_PRECISION, RoundingMode.HALF_EVEN) } - - protected BigDecimal getPriceAfterCurrencyConversion(BigDecimal value, - Currency currencyFrom, Currency currencyTo, - CurrencyRatesResponse currencyRatesResponse) { - def currencyRate = currencyRatesResponse.rates[currencyFrom.value][currencyTo.value] - def convertedValue = value * currencyRate - convertedValue.setScale(CURRENCY_CONVERSION_PRECISION, RoundingMode.HALF_EVEN) - } } 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..b569514d0c4 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,14 +1,15 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse +import org.prebid.server.functional.model.config.AccountPriceFloorsConfig +import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.ImpExtPrebidFloors 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.service.PrebidServerService -import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.BOGUS @@ -20,26 +21,20 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.NON import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID -import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } private static final String GENERAL_ERROR_METRIC = "price-floors.general.err" - private static final Map CURRENCY_CONVERTER_CONFIG = ["auction.ad-server-currency" : "USD", - "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"] - private final PrebidServerService currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + - CURRENCY_CONVERTER_CONFIG) + + private static PrebidServerService currencyFloorsPbsService + + def setupSpec() { + currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } def "PBS should update bidFloor, bidFloorCur for signalling when request.cur is specified"() { given: "Default BidRequest with cur" @@ -51,6 +46,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -60,7 +59,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,13 +91,13 @@ 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() and: "Bid response with 2 bids: price < floorMin, price = floorMin" - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorsResponse.modelGroups[0].currency, bidRequest.cur[0], currencyRatesResponse) def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = EUR @@ -135,9 +134,13 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Get currency rates" def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur, floorValur lower then floorMin" def floorProviderCur = EUR - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorMin, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorMin, bidRequest.ext.prebid.floors.floorMinCur, floorProviderCur, currencyRatesResponse) def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -147,7 +150,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) @@ -182,6 +185,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = EUR def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -191,7 +198,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,11 +245,17 @@ 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: BASIC_FETCH_URL + bidRequest.accountId, enabled: priceFloorsSnakeCase)) } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -253,6 +266,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"() { @@ -274,6 +292,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -307,14 +329,14 @@ 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() and: "Bid response with 2 bids: price < floorMin, price = floorMin" def bidResponseCur = GBP - def convertedMinFloorValueGbp = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValueGbp = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorCur, bidResponseCur, currencyRatesResponse) def winBidPrice = convertedMinFloorValueGbp + 0.1 def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -336,7 +358,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } and: "PBS should suppress bids lower than floorRuleValue" - def convertedFloorValueEur = getPriceAfterCurrencyConversion(winBidPrice, + def convertedFloorValueEur = CurrencyUtil.getPriceAfterCurrencyConversion(winBidPrice, bidResponseCur, requestCur, currencyRatesResponse) assert response.seatbid?.first()?.bid?.collect { it.price } == [convertedFloorValueEur] assert response.cur == bidRequest.cur[0] @@ -358,6 +380,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = BOGUS def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -367,7 +393,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) @@ -404,16 +430,22 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain floorMin, floorMinCur, currency from request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].ext.prebid.floors.floorMinCur == EUR - imp[0].ext.prebid.floors.floorMin == FLOOR_MIN + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { ext.prebid.floors.floorMinCur == EUR ext.prebid.floors.floorMin == FLOOR_MIN } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should return warning when both floorMinCur and floorMinCur exist and they're different"() { @@ -436,12 +468,14 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { ["imp[].ext.prebid.floors.floorMinCur and ext.prebid.floors.floorMinCur has different values"] and: "Bidder request should contain floorMinCur, floorMin from request" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].ext.prebid.floors.floorMinCur == EUR - imp[0].ext.prebid.floors.floorMin == FLOOR_MIN + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { ext.prebid.floors.floorMinCur == JPY ext.prebid.floors.floorMin == FLOOR_MIN } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should choose floorMin from imp[0].ext.prebid.floors when imp[0].ext.prebid.floors is present"() { @@ -457,17 +491,22 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain floorMin, floorValue, bidFloor, bidFloorCur" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - imp[0].ext.prebid.floors.floorMinCur == USD - imp[0].ext.prebid.floors.floorMin == impExtPrebidFloorMin - imp[0].ext.prebid.floors.floorValue == impExtPrebidFloorMin + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { imp[0].bidFloor == impExtPrebidFloorMin imp[0].bidFloorCur == USD } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should choose floorMin from ext.prebid.floors when imp[0].ext.prebid.floor.floorMin is absent"() { @@ -482,16 +521,21 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) then: "Bidder request should contain bidFloorCur, bidFloor, floorValue" - verifyAll(bidder.getBidderRequest(bidRequest.id)) { - !imp[0].ext.prebid.floors.floorMinCur - !imp[0].ext.prebid.floors.floorMin - imp[0].ext.prebid.floors.floorValue == extPrebidFloorMin + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest) { imp[0].bidFloor == extPrebidFloorMin imp[0].bidFloorCur == USD } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } } 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..cec8bb8fa41 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 { @@ -72,7 +77,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { def response = floorsPbsService.sendAmpRequest(ampRequest) then: "PBS should suppress bids lower than floorRuleValue" - def bidPrice = getRoundedTargetingValueWithDefaultPrecision(floorValue) + def bidPrice = getRoundedTargetingValueWithDownPrecision(floorValue) verifyAll(response) { targeting["hb_pb_generic"] == bidPrice targeting["hb_pb"] == bidPrice @@ -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..b40f6e8cac2 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 @@ -2,7 +2,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.StoredRequest -import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.Rule import org.prebid.server.functional.model.request.amp.AmpRequest @@ -17,30 +17,43 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 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.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER 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.FetchStatus.ERROR +import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH +import static org.prebid.server.functional.model.request.auction.Location.NO_DATA import static org.prebid.server.functional.model.request.auction.Location.REQUEST import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { - private static final int MAX_ENFORCE_FLOORS_RATE = 100 + private static final int ENFORCE_FLOORS_RATE_MAX = 100 private static final int DEFAULT_MAX_AGE_SEC = 600 private static final int DEFAULT_PERIOD_SEC = 300 - private static final int MIN_TIMEOUT_MS = 10 - private static final int MAX_TIMEOUT_MS = 10000 - private static final int MIN_SKIP_RATE = 0 - private static final int MAX_SKIP_RATE = 100 - private static final int MIN_DEFAULT_FLOOR_VALUE = 0 - private static final int MIN_FLOOR_MIN = 0 - - private static final Closure INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" } + private static final int TIMEOUT_MS_MIN = 10 + private static final int TIMEOUT_MS_MAX = 10000 + private static final int SKIP_RATE_MIN = 0 + private static final int SKIP_RATE_MAX = 100 + private static final int USE_FETCH_DATA_RATE_MIN = 0 + private static final int USE_FETCH_DATA_RATE_MAX = 100 + private static final int DEFAULT_FLOOR_VALUE_MIN = 0 + private static final int FLOOR_MIN = 0 + private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure" + private static final String PRICE_FLOOR_VALUES_MISSING = 'Price floor rules values can\'t be null or empty, but were null' + private static final String MODEL_WEIGHT_INVALID = "Price floor modelGroup modelWeight must be in range(1-100), but was %s" + private static final String SKIP_RATE_INVALID = "Price floor modelGroup skipRate must be in range(0-100), but was %s" + private static Instant startTime + + def setupSpec() { + startTime = Instant.now() + } def "PBS should activate floors feature when price-floors.enabled = true in PBS config"() { given: "Pbs with PF configuration" @@ -50,17 +63,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should signal bids" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -75,19 +88,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch and fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.enabled = accountConfigEnabled } accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should no fetching, no signaling, no enforcing" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert !bidderRequest.imp[0].bidFloor @@ -105,7 +118,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, without fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.url = null } accountDao.save(account) @@ -115,9 +128,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { then: "PBS should log error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, bidRequest.site.publisher.id) + def floorsLogs = getLogsByText(logs, bidRequest.accountId) assert floorsLogs.size() == 1 - assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.site.publisher.id") + assert floorsLogs.first().contains("alformed fetch.url: 'null' passed for account $bidRequest.accountId") and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() @@ -132,8 +145,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) 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 + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(bidRequest.accountId, DEFAULT_MAX_AGE_SEC - 1) } accountDao.save(account) @@ -142,10 +155,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid.isEmpty() + + where: + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSec: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSecSnakeCase: max) }] } def "PBS should validate fetch.period-sec from account config"() { @@ -153,7 +170,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, periodSec in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetchConfig(DEFAULT_PERIOD_SEC, defaultAccountConfigSettings.auction.priceFloors.fetch.maxAgeSec) } @@ -164,14 +181,16 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() 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"() { @@ -179,7 +198,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxFileSizeKb in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxFileSizeKb = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -187,9 +206,9 @@ 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -200,7 +219,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, maxRules in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.maxRules = PBSUtils.randomNegativeNumber } accountDao.save(account) @@ -210,7 +229,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() @@ -221,8 +240,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, timeoutMs in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.fetch = fetchConfig(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS) + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch = fetchConfig(TIMEOUT_MS_MIN, TIMEOUT_MS_MAX) } accountDao.save(account) @@ -231,14 +250,16 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() 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"() { @@ -246,8 +267,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url, enforceFloorsRate in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { - config.auction.priceFloors.enforceFloorsRate = enforceFloorsRate + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.tap { + it.enforceFloorsRate = enforceFloorsRate + it.enforceFloorsRateSnakeCase = enforceFloorsRateSnakeCase + } } accountDao.save(account) @@ -256,13 +280,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { 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 + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 and: "PBS floors validation failure should not reject the entire auction" assert !response.seatbid?.isEmpty() where: - enforceFloorsRate << [PBSUtils.randomNegativeNumber, MAX_ENFORCE_FLOORS_RATE + 1] + enforceFloorsRate | enforceFloorsRateSnakeCase + PBSUtils.randomNegativeNumber | null + ENFORCE_FLOORS_RATE_MAX + 1 | null + null | PBSUtils.randomNegativeNumber + null | ENFORCE_FLOORS_RATE_MAX + 1 } def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() { @@ -272,7 +300,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = true } accountDao.save(account) @@ -281,7 +309,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 } def "PBS should process floors from request when price-floors.fetch.enabled = false in account config"() { @@ -289,7 +317,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch = fetch } accountDao.save(account) @@ -304,20 +332,23 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsPbsService.sendAuctionRequest(bidRequest) then: "PBS should not fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 0 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor where: - fetch << [new PriceFloorsFetch(enabled: false, url: basicFetchUrl), new PriceFloorsFetch(url: basicFetchUrl)] + fetch << [new PriceFloorsFetch(enabled: false, url: BASIC_FETCH_URL), new PriceFloorsFetch(url: BASIC_FETCH_URL)] } - def "PBS should fetch data from provider when use-dynamic-data = true"() { + def "PBS should fetch data from provider when use-dynamic-data enabled"() { 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)]) @@ -328,9 +359,9 @@ 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 + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData + config.auction.priceFloors.useDynamicDataSnakeCase = accountUseDynamicDataSnakeCase } accountDao.save(account) @@ -339,24 +370,321 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, 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 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() 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 fetch data from provider when use-dynamic-data enabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + imp[0].bidFloor == floorValue + imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency + + ext?.prebid?.floors?.location == FETCH + ext?.prebid?.floors?.fetchStatus == SUCCESS + ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider + + ext?.prebid?.floors?.skipRate == floorsResponse.skipRate + ext?.prebid?.floors?.data == floorsResponse + } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data disabled and useFetchDataRate at max value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with disabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = false + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MAX + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS shouldn't fetch data from provider when use-dynamic-data enabled and useFetchDataRate at min value"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled use-dynamic-data parameter" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = USE_FETCH_DATA_RATE_MIN + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + when: "PBS processes auction request" + floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "Bidder request shouldn't contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + !ext?.prebid?.floors?.floorProvider + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == SUCCESS + } + } + + def "PBS should log error and increase metrics when useFetchDataRate have invalid value"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default BidRequest" + def bidRequest = BidRequest.getDefaultBidRequest() + + and: "Account with enabled fetch and fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = accounntUseFetchDataRate + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "metric should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "PBS should not add warning or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "PBS log should contain error" + def logs = floorsPbsService.getLogsByTime(startTime) + def message = "Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate" + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId") + assert floorsLogs.size() == 1 + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + !imp[0].bidFloor + !imp[0].bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.floorProvider + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == ERROR + } + + where: + accounntUseFetchDataRate << [PBSUtils.getRandomNegativeNumber(-USE_FETCH_DATA_RATE_MAX, USE_FETCH_DATA_RATE_MIN), + PBSUtils.getRandomNumber(USE_FETCH_DATA_RATE_MAX + 1) + ] + } + + def "PBS should log merged error and increase metrics when useFetchDataRate have invalid value from provider and request"() { + given: "BidRequest with invalid floors" + def requestUseFetchDataRate = PBSUtils.getRandomNegativeNumber() + def bidRequest = getBidRequestWithFloors().tap { + it.ext.prebid.floors.data.useFetchDataRate = requestUseFetchDataRate + } + + and: "Account with enabled fetch and fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = true + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Set Floors Provider response" + def providerUseFetchDataRate = PBSUtils.getRandomNegativeNumber() + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + useFetchDataRate = providerUseFetchDataRate + } + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, ERROR) + + and: "Test start time" + def startTime = Instant.now() + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "metric should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[FETCH_FAILURE_METRIC] == 1 + + and: "PBS should add single warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == + [WARNING_MESSAGE("Price floor data useFetchDataRate must be in range(0-100), but was $requestUseFetchDataRate")] + + and: "PBS should not add error to request" + assert !response.ext.errors + + and: "PBS should fetch data" + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 + + and: "PBS log should contain combined error log" + def logs = floorsPbsService.getLogsByTime(startTime) + def fetchingErrorLogs = getLogsByText(logs, "Price Floors can't be resolved for account $bidRequest.accountId " + + "and request $bidRequest.id, reason: Price floors processing failed: Failed to fetch price floor from provider " + + "for fetch.url '$BASIC_FETCH_URL${bidRequest.accountId}', with a reason: " + + "Price floor data useFetchDataRate must be in range(0-100), but was $providerUseFetchDataRate. " + + "Following parsing of request price floors is failed: " + + "Price floor data useFetchDataRate must be in range(0-100), but was $requestUseFetchDataRate") + assert fetchingErrorLogs.size() == 1 + + and: "Floors validation failure cannot reject the entire auction" + assert !response.seatbid?.isEmpty() + + and: "Bidder request should contain floors data from floors provider" + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last + verifyAll(bidderRequest) { + imp.bidFloor == bidRequest.imp.bidFloor + imp.bidFloorCur == bidRequest.imp.bidFloorCur + + !imp[0].ext?.prebid?.floors?.floorRule + !imp[0].ext?.prebid?.floors?.floorRuleValue + !imp[0].ext?.prebid?.floors?.floorValue + + !ext?.prebid?.floors?.floorProvider + !ext?.prebid?.floors?.skipRate + !ext?.prebid?.floors?.data + ext?.prebid?.floors?.location == NO_DATA + ext?.prebid?.floors?.fetchStatus == ERROR + } } def "PBS should process floors from request when use-dynamic-data = false"() { @@ -371,19 +699,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with fetch.enabled, fetch.url, useDynamicData in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.useDynamicData = accountUseDynamicData } accountDao.save(account) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(pbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, pbsService) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should fetch data from floors provider" - assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "Bidder request should contain bidFloor from request" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() @@ -407,7 +735,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -415,7 +743,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, BAD_REQUEST_400) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(floorsPbsService, bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -427,12 +755,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to request, provider respond with status 400" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 request for " + - "account $accountId, provider respond with status 400") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -449,7 +776,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -457,6 +784,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def invalidJson = "{{}}" floorsProvider.setResponse(accountId, invalidJson) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -467,12 +797,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to parse price floor response, cause: DecodeException: Failed to decode" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 " + - "response for account $accountId, cause: DecodeException: Failed to decode") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -489,13 +818,16 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response with invalid json" floorsProvider.setResponse(accountId, "") + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -506,12 +838,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Failed to parse price floor response, response body can not be empty" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 " + - "response for account $accountId, response body can not be empty" as String) + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -528,7 +859,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -538,6 +869,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -548,12 +882,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor rules should contain at least one model group" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules should contain " + - "at least one model group " as String) + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -570,7 +903,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -580,6 +913,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -591,17 +927,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules values can't " + - "be null or empty, but were null" as String) + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, PRICE_FLOOR_VALUES_MISSING)) and: "Floors validation failure cannot reject the entire auction" 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() @@ -612,10 +946,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def maxRules = 1 def account = getAccountWithEnabledFetch(accountId).tap { - config.auction.priceFloors.fetch.maxRules = maxRules + config.auction.priceFloors.fetch = fetchConfig(accountId, maxRules) } accountDao.save(account) @@ -625,6 +959,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } floorsProvider.setResponse(accountId, floorsResponse) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -635,15 +972,18 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor rules number 2 exceeded its maximum number $maxRules" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 rules number " + - "2 exceeded its maximum number $maxRules") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() + + where: + fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRules: max) }, + { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRulesSnakeCase: max) }] } def "PBS should log error and increase #FETCH_FAILURE_METRIC when fetch request exceeds fetch.timeout-ms"() { @@ -660,13 +1000,16 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response with timeout" floorsProvider.setResponseWithTimeout(accountId) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, pbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = pbsService.sendAuctionRequest(bidRequest) @@ -678,16 +1021,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = pbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "Price floor fetching failed for account $bidRequest.accountId: " + + "Fetch price floor request timeout for fetch.url '$BASIC_FETCH_URL$accountId' exceeded") assert floorsLogs.size() == 1 - assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$basicFetchUrl$accountId', " + - "account $accountId exceeded") and: "Floors validation failure cannot reject the entire auction" 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() @@ -698,10 +1040,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with maxFileSizeKb in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId 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) @@ -710,6 +1052,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def responseSize = convertKilobyteSizeToByte(maxSize) + 75 floorsProvider.setResponse(accountId, floorsResponse, ["Content-Length": responseSize as String]) + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -720,32 +1065,36 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Response size $responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 : Response size " + - "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() + + where: + fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKb: maxKbSize) }, + { String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }] } def "PBS should prefer data from stored request when request doesn't contain floors data"() { given: "Default BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = request.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) } and: "Default stored request with floors" def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(accountId).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -759,13 +1108,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { imp[0].bidFloor == storedRequestModel.ext.prebid.floors.data.modelGroups[0].values[rule] imp[0].bidFloorCur == storedRequestModel.ext.prebid.floors.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == - storedRequestModel.ext.prebid.floors.data.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == - storedRequestModel.ext.prebid.floors.data.modelGroups[0].values[rule] - imp[0].ext?.prebid?.floors?.floorValue == - storedRequestModel.ext.prebid.floors.data.modelGroups[0].values[rule] - ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == storedRequestModel.ext.prebid.floors.floorMin @@ -773,10 +1115,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.data == storedRequestModel.ext.prebid.floors.data } + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors + where: - request | accountId | bidRequestWithFloors - BidRequest.defaultBidRequest | request.site.publisher.id | bidRequestWithFloors - BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id | getBidRequestWithFloors(APP) + request | bidRequestWithFloors + BidRequest.defaultBidRequest | getBidRequestWithFloors(SITE) + BidRequest.getDefaultBidRequest(APP) | getBidRequestWithFloors(APP) } def "PBS should prefer data from request when fetch is disabled in account config"() { @@ -784,7 +1129,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -804,23 +1149,22 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { imp[0].bidFloor == bidRequest.ext.prebid.floors.data.modelGroups[0].values[rule] imp[0].bidFloorCur == bidRequest.ext.prebid.floors.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == bidRequest.ext.prebid.floors.data.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == bidRequest.ext.prebid.floors.data.modelGroups[0].values[rule] - imp[0].ext?.prebid?.floors?.floorValue == bidRequest.ext.prebid.floors.data.modelGroups[0].values[rule] - ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin ext?.prebid?.floors?.floorProvider == bidRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == bidRequest.ext.prebid.floors.data } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should prefer data from stored request when fetch is disabled in account config for amp request"() { given: "Default AmpRequest" def ampRequest = AmpRequest.defaultAmpRequest - and: "Default stored request with floors " + and: "Default stored request with floors" def ampStoredRequest = storedRequestWithFloors def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) @@ -840,25 +1184,22 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { imp[0].bidFloor == ampStoredRequest.ext.prebid.floors.data.modelGroups[0].values[rule] imp[0].bidFloorCur == ampStoredRequest.ext.prebid.floors.data.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == - ampStoredRequest.ext.prebid.floors.data.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == - ampStoredRequest.ext.prebid.floors.data.modelGroups[0].values[rule] - imp[0].ext?.prebid?.floors?.floorValue == - ampStoredRequest.ext.prebid.floors.data.modelGroups[0].values[rule] - ext?.prebid?.floors?.location == REQUEST ext?.prebid?.floors?.fetchStatus == NONE ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin ext?.prebid?.floors?.floorProvider == ampStoredRequest.ext.prebid.floors.data.floorProvider ext?.prebid?.floors?.data == ampStoredRequest.ext.prebid.floors.data } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should prefer data from floors provider when floors data is defined in both request and stored request"() { given: "BidRequest with storedRequest" + def storedRequestId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithFloors.tap { - ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber) + ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) ext.prebid.floors.floorMin = FLOOR_MIN } @@ -866,11 +1207,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def storedRequestModel = bidRequestWithFloors and: "Save storedRequest into DB" - def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel) + def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel) storedRequestDao.save(storedRequest) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -878,21 +1219,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, 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() + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last verifyAll(bidderRequest) { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue - imp[0].ext?.prebid?.floors?.floorValue == floorValue - ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == bidRequest.ext.prebid.floors.floorMin @@ -901,6 +1238,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should prefer data from floors provider when floors data is defined in stored request for amp request"() { @@ -934,10 +1274,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue - imp[0].ext?.prebid?.floors?.floorValue == floorValue - ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS ext?.prebid?.floors?.floorMin == ampStoredRequest.ext.prebid.floors.floorMin @@ -946,6 +1282,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should periodically fetch floor rules when previous response from floors provider is #description"() { @@ -957,20 +1296,20 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) then: "PBS should cache data from data provider" - assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1 + assert floorsProvider.getRequestCount(bidRequest.accountId) == 1 and: "PBS should periodically fetch data from data provider" - PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.app.publisher.id) > 1 }, 7000, 3000) + PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.accountId) > 1 }, 7000, 3000) where: description | floorsResponse @@ -990,7 +1329,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider #description response" @@ -998,7 +1337,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorsResponse = PriceFloorData.priceFloorData.tap { modelGroups[0].values = [(rule): floorValue] } - floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) when: "PBS processes auction request" pbsService.sendAuctionRequest(bidRequest) @@ -1019,9 +1358,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue - imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS @@ -1031,23 +1367,30 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should validate rules from request when floorMin from request is invalid"() { given: "Default BidRequest with floorMin" def floorValue = PBSUtils.randomFloorValue - def invalidFloorMin = MIN_FLOOR_MIN - 1 + def invalidFloorMin = FLOOR_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue ext.prebid.floors.floorMin = invalidFloorMin } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1055,11 +1398,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor floorMin " + - "must be positive float, but was $invalidFloorMin "] + and: "Response should contain warning" + def message = "Price floor floorMin must be positive float, but was $invalidFloorMin" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] } def "PBS should validate rules from request when request doesn't contain modelGroups"() { @@ -1071,11 +1413,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1083,11 +1429,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor rules " + - "should contain at least one model group "] + and: "Response should contain warning" + def message = "Price floor rules should contain at least one model group" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] } def "PBS should validate rules from request when request doesn't contain values"() { @@ -1099,11 +1444,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1111,11 +1460,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor rules values " + - "can't be null or empty, but were null "] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(PRICE_FLOOR_VALUES_MISSING)] } def "PBS should validate rules from request when modelWeight from request is invalid"() { @@ -1123,7 +1470,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] @@ -1131,11 +1478,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1143,11 +1494,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + - "must be in range(1-100), but was $invalidModelWeight "] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))] + where: invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] } @@ -1160,7 +1510,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def ampStoredRequest = storedRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups.first().modelWeight = invalidModelWeight ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] @@ -1175,6 +1525,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest) + bidder.setResponse(ampStoredRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAmpRequest(ampRequest) @@ -1182,11 +1536,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(ampStoredRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " + - "must be in range(1-100), but was $invalidModelWeight "] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))] where: invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1] @@ -1197,7 +1549,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = 0 ext.prebid.floors.data.skipRate = 0 @@ -1207,7 +1559,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1215,6 +1567,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1222,14 +1578,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor root skipRate " + - "must be in range(0-100), but was $invalidSkipRate "] + and: "Response should contain warning" + def message = "Price floor root skipRate must be in range(0-100), but was $invalidSkipRate" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when data skipRate from request is invalid"() { @@ -1237,7 +1592,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = 0 ext.prebid.floors.data.skipRate = invalidSkipRate @@ -1247,7 +1602,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1255,6 +1610,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1262,14 +1621,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor data skipRate " + - "must be in range(0-100), but was $invalidSkipRate "] + and: "Response should contain warning" + def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from request is invalid"() { @@ -1277,7 +1635,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def floorValue = PBSUtils.randomFloorValue def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].skipRate = invalidSkipRate ext.prebid.floors.data.skipRate = 0 @@ -1287,7 +1645,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) @@ -1295,6 +1653,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1302,35 +1664,37 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor modelGroup skipRate " + - "must be in range(0-100), but was $invalidSkipRate "] + and: "Response should contain warning" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(SKIP_RATE_INVALID.formatted(invalidSkipRate))] where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should validate rules from request when default floor value from request is invalid"() { given: "Default BidRequest with default floor value" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloorValue = DEFAULT_FLOOR_VALUE_MIN - 1 def bidRequest = bidRequestWithFloors.tap { imp[0].bidFloor = floorValue - ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup + ext.prebid.floors.data.modelGroups << FloorModelGroup.modelGroup ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1] ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2] - ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + ext.prebid.floors.data.modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } and: "Account with disabled fetch in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap { + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { config.auction.priceFloors.fetch.enabled = false } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1338,11 +1702,230 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - and: "Response should contain error" - assert response.ext?.errors[PREBID]*.code == [999] - assert response.ext?.errors[PREBID]*.message == - ["Failed to parse price floors from request, with a reason : Price floor modelGroup default " + - "must be positive float, but was $invalidDefaultFloorValue "] + and: "Response should contain warning" + def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloorValue" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + } + + def "PBS shouldn't emit error in log and response when floors is not in request and floors fetching disabled for account"() { + given: "Account with disabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.fetch.url = null + } + accountDao.save(account) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't log warning or errors" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS shouldn't log a errors" + def message = "Price floor rules data must be present" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert !floorsLogs.size() + + and: "PBS request on response object status should be in progress" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == NONE + + and: "PBS bidderRequest status should be in progress" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus == [NONE] + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + + where: + bidRequest << [BidRequest.getDefaultBidRequest(), getBidRequestWithFloors().tap { it.ext.prebid.floors = null }] + } + + def "PBS should emit error in log and response when floor data is empty and floors fetching disabled for account and #requestFloorEnabled for request"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + it.ext.prebid.floors.enabled = requestFloorEnabled + it.ext.prebid.floors.data = null + } + + and: "Account with disabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + it.config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor rules data must be present" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should not add errors" + assert !bidResponse.ext.errors + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "PBS request on response object status should be not in progress" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == NONE + + and: "PBS request status shouldn't be in progress" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus == [NONE] + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestFloorEnabled << [null, true] + } + + def "PBS shouldn't emit error in log and response when floor data is empty and floors fetching disabled for account and floors disabled for request"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + it.ext.prebid.floors.enabled = false + it.ext.prebid.floors.data = null + } + + and: "Account with disabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + it.config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't log warning or errors" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS shouldn't log a errors" + def message = "Price floor rules data must be present" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert !floorsLogs.size() + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + } + + def "PBS shouldn't emit error in log and response when data is invalid and floors fetching enabled for account"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.enabled = requestEnabledFloors + ext.prebid.floors.data = null + } + + and: "Account with enabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId) + accountDao.save(account) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't log warning or errors" + assert !bidResponse.ext?.warnings + assert !bidResponse.ext?.errors + + and: "PBS shouldn't log a errors" + def message = "Price floor rules data must be present" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert !floorsLogs.size() + + and: "PBS request on response object status should be in progress" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == INPROGRESS + + and: "PBS bidderRequest status should be in progress" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus == [INPROGRESS] + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + + where: + requestEnabledFloors << [null, true] + } + + def "PBS shouldn't emit error in log and response when data is invalid and floors disabled for account"() { + given: "Default BidRequest with empty floors.data" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data = null + } + + and: "Account with disabled fetching" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.enabled = false + config.auction.priceFloors.fetch.enabled = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should not add warning or errors" + assert !bidResponse.ext.warnings + assert !bidResponse.ext.errors + + and: "PBS request on response object status should be empty" + assert getRequests(bidResponse)[GENERIC.value]?.first?.ext?.prebid?.floors?.fetchStatus == null + + and: "PBS request status should be empty" + def bidderRequest = bidder.getBidderRequests(bidRequest.id) + assert bidderRequest.ext.prebid.floors.fetchStatus.every { it == null } + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] } def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() { @@ -1354,7 +1937,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.getDefaultBidRequest(APP) and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.app.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1366,7 +1949,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) @@ -1381,9 +1964,6 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { verifyAll(bidderRequest) { imp[0].bidFloor == floorValue imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency - imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0] - imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue - imp[0].ext?.prebid?.floors?.floorValue == floorValue ext?.prebid?.floors?.location == FETCH ext?.prebid?.floors?.fetchStatus == SUCCESS @@ -1393,6 +1973,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { ext?.prebid?.floors?.skipRate == floorsResponse.skipRate ext?.prebid?.floors?.data == floorsResponse } + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should reject fetch when modelWeight from floors provider is invalid"() { @@ -1406,14 +1989,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups.first().modelWeight = invalidModelWeight modelGroups.last().values = [(rule): floorValue] @@ -1422,7 +2005,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1440,11 +2023,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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 modelWeight" + - " must be in range(1-100), but was $invalidModelWeight") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, MODEL_WEIGHT_INVALID.formatted(invalidModelWeight))) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1464,14 +2045,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].skipRate = 0 skipRate = invalidSkipRate @@ -1481,7 +2062,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1498,18 +2079,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor data skipRate must be in range(0-100), but was $invalidSkipRate" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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" + - " must be in range(0-100), but was $invalidSkipRate") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() { @@ -1523,14 +2103,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].skipRate = invalidSkipRate skipRate = 0 @@ -1540,7 +2120,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1558,17 +2138,15 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { and: "PBS log should contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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" + - " must be in range(0-100), but was $invalidSkipRate") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, SKIP_RATE_INVALID.formatted(invalidSkipRate))) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() where: - invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1] + invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1] } def "PBS should reject fetch when default floor value from floors provider is invalid"() { @@ -1582,24 +2160,24 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue - def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1 + def invalidDefaultFloor = DEFAULT_FLOOR_VALUE_MIN - 1 def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups[0].defaultFloor = invalidDefaultFloor modelGroups.last().values = [(rule): floorValue] - modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE + modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN } floorsProvider.setResponse(accountId, floorsResponse) and: "PBS fetch rules from floors provider" - cacheFloorsProviderRules(bidRequest) + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, NONE) when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) @@ -1616,12 +2194,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { assert metrics[FETCH_FAILURE_METRIC] == 1 and: "PBS log should contain error" + def message = "Price floor modelGroup default must be positive float, but was $invalidDefaultFloor" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.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" + - " must be positive float, but was $invalidDefaultFloor") + assert floorsLogs[0].contains(FETCHING_FLOORS_ERROR_LOG(bidRequest, message)) and: "Floors validation failure cannot reject the entire auction" assert !response.seatbid?.isEmpty() @@ -1632,7 +2209,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = bidRequestWithFloors and: "Account with enabled fetch, fetch.url in the DB" - def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + def account = getAccountWithEnabledFetch(bidRequest.accountId) accountDao.save(account) and: "Set Floors Provider response" @@ -1642,7 +2219,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { modelGroups[0].currency = modelGroupCurrency currency = dataCurrency } - floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + floorsProvider.setResponse(bidRequest.accountId, floorsResponse) and: "PBS fetch rules from floors provider" cacheFloorsProviderRules(bidRequest) @@ -1668,7 +2245,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1681,14 +2258,14 @@ 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) then: "PBS log should not contain error" def logs = floorsPbsService.getLogsByTime(startTime) - def floorsLogs = getLogsByText(logs, basicFetchUrl) + def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL) assert floorsLogs.size() == 0 and: "Bidder request should contain floors data from floors provider" @@ -1702,7 +2279,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { def bidRequest = BidRequest.defaultBidRequest and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1714,7 +2291,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) @@ -1738,7 +2315,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1776,7 +2353,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } and: "Account with enabled fetch, fetch.url in the DB" - def accountId = bidRequest.site.publisher.id + def accountId = bidRequest.accountId def account = getAccountWithEnabledFetch(accountId) accountDao.save(account) @@ -1805,6 +2382,91 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec { } } + def "PBS should validate fetch.max-schema-dims from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxSchemaDims in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.fetch.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + null | PBSUtils.randomNegativeNumber + null | PBSUtils.getRandomNumber(20) + PBSUtils.randomNegativeNumber | null + PBSUtils.getRandomNumber(20) | null + } + + def "PBS should validate price-floor.max-rules from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxRules in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxRules | maxRulesSnakeCase + null | PBSUtils.randomNegativeNumber + PBSUtils.randomNegativeNumber | null + } + + def "PBS should validate price-floor.max-schema-dims from account config and not reject entire auction"() { + given: "Default BidRequest" + def bidRequest = BidRequest.defaultBidRequest + + and: "Account with enabled fetch, maxSchemaDims in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Metric alerts.account_config.ACCOUNT.price-floors should be update" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + + and: "PBS floors validation failure should not reject the entire auction" + assert !response.seatbid?.isEmpty() + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + null | PBSUtils.randomNegativeNumber + null | PBSUtils.getRandomNumber(20) + PBSUtils.randomNegativeNumber | null + PBSUtils.getRandomNumber(20) | null + } + static int convertKilobyteSizeToByte(int kilobyteSize) { kilobyteSize * 1024 } 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..b586688398b 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,13 +1,19 @@ 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.config.AlternateBidderCodes +import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.pricefloors.Country +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.MediaType -import org.prebid.server.functional.model.pricefloors.ModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule +import org.prebid.server.functional.model.request.auction.Amx 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 @@ -19,11 +25,17 @@ import org.prebid.server.functional.model.request.auction.Imp 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.PrebidStoredRequest +import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.util.PBSUtils +import java.time.Instant + 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.AMX 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 +44,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 +62,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 { @@ -81,8 +95,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should correspond to appropriate rule" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == floorValue - assert bidderRequest.imp[0].ext.prebid.floors.floorRule == rule - assert bidderRequest.imp[0].ext.prebid.floors.floorRuleValue == floorValue + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should support different delimiters for floor rules"() { @@ -163,7 +178,10 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { } def "PBS should consider rules file invalid when rules file contains an unrecognized dimension in the schema"() { - given: "BidRequest with domain" + given: "Test start time" + def startTime = Instant.now() + + and: "BidRequest with domain" def domain = PBSUtils.randomString def accountId = PBSUtils.randomString def bidRequest = BidRequest.defaultBidRequest.tap { @@ -178,7 +196,7 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups[0].schema = new PriceFloorSchema(fields: [BOGUS]) modelGroups[0].values = [(new Rule(domain: domain).rule): floorValue + 0.1] modelGroups[1].schema = new PriceFloorSchema(fields: [DOMAIN]) @@ -204,9 +222,15 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert bidderRequest.ext?.prebid?.floors?.location == NO_DATA assert bidderRequest.ext?.prebid?.floors?.fetchStatus == ERROR - and: "PBS should not contain errors, warnings" - assert !response.ext?.warnings + and: "PBS should not contain errors or warnings" assert !response.ext?.errors + assert !response.ext?.warnings + + and: "PBS should log a warning" + def logs = floorsPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "Cannot deserialize value of type " + + "`org.prebid.server.floors.model.PriceFloorField` " + + "from String \"bogus\": not one of the values accepted for Enum class").size() == 1 and: "PBS should not reject the entire auction" assert !response.seatbid.isEmpty() @@ -270,10 +294,8 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { where: bidRequest | bothFloorValue | bannerFloorValue | videoFloorValue - bidRequestWithMultipleMediaTypes | 0.6 | PBSUtils.randomFloorValue | - PBSUtils.randomFloorValue - BidRequest.defaultBidRequest | PBSUtils.randomFloorValue | 0.6 | - PBSUtils.randomFloorValue + bidRequestWithMultipleMediaTypes | 0.6 | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue + BidRequest.defaultBidRequest | PBSUtils.randomFloorValue | 0.6 | PBSUtils.randomFloorValue BidRequest.defaultVideoRequest | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue | 0.6 } @@ -284,8 +306,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(width: lowerWidth, height: lowerHigh), + new Format(width: higherWidth, height: higherHigh)] } and: "Account with enabled fetch, fetch.url in the DB" @@ -352,20 +374,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(width: 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.width = 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.width = widthVal + video.height = heightVal } } } @@ -405,32 +427,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 +504,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 +555,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 +951,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 { @@ -939,9 +967,9 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC 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 +998,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 +1016,211 @@ 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 + } + + def "PBS should populate seatNonBid when bid rejected due to floor and alternate bidder code specified"() { + given: "PBS config with floors config" + def config = FLOORS_CONFIG + + ["adapters.amx.enabled" : "true", + "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def pbsService = pbsServiceFactory.getService(config) + + and: "Default bid request" + def bidRequest = BidRequest.defaultBidRequest.tap { + imp[0].ext.prebid.bidder.tap { + generic = null + amx = new Amx() + } + ext.prebid.tap { + floors = new ExtPrebidFloors(enforcement: new ExtPrebidPriceFloorEnforcement(enforcePbs: true)) + returnAllBidStatus = true + alternateBidderCodes = new AlternateBidderCodes( + enabled: true, + bidders: [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [GENERIC])]) + } + } + + and: "Account with enabled fetch, fetch.url in the DB" + def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) + accountDao.save(account) + + and: "Set Floors Provider response" + def floorValue = PBSUtils.randomFloorValue + def floorsResponse = PriceFloorData.priceFloorData.tap { + modelGroups[0].values = [(rule): floorValue] + } + floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse) + + and: "PBS cache rules" + cacheFloorsProviderRules(bidRequest, floorValue, pbsService, AMX) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid.first().bid.first().tap { + price = floorValue - 0.1 + ext = new BidExt(bidderCode: GENERIC) + } + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should contain seatNonBid" + def seatNonBids = response.ext.seatnonbid + assert seatNonBids.size() == 1 + + def seatNonBid = seatNonBids[0] + assert seatNonBid.seat == GENERIC + assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id + assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_DUE_TO_PRICE_FLOOR + assert seatNonBid.nonBid.size() == bidResponse.seatbid[0].bid.size() + } + + 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..b06c2530242 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 @@ -2,7 +2,7 @@ package org.prebid.server.functional.tests.pricefloors import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.pricefloors.Country -import org.prebid.server.functional.model.pricefloors.ModelGroup +import org.prebid.server.functional.model.pricefloors.FloorModelGroup import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.pricefloors.PriceFloorSchema import org.prebid.server.functional.model.pricefloors.Rule @@ -18,6 +18,9 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.MediaType import org.prebid.server.functional.util.PBSUtils +import java.math.RoundingMode +import java.time.Instant + import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.bidder.BidderName.GENERIC @@ -27,10 +30,20 @@ import static org.prebid.server.functional.model.pricefloors.MediaType.VIDEO import static org.prebid.server.functional.model.pricefloors.PriceFloorField.MEDIA_TYPE import static org.prebid.server.functional.model.pricefloors.PriceFloorField.SITE_DOMAIN import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP +import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { - def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() { + private static final int MAX_SCHEMA_DIMENSIONS_SIZE = 1 + private static final int MAX_RULES_SIZE = 1 + private static Instant startTime + + def setupSpec() { + startTime = Instant.now() + } + + def "PBS should skip signaling for request with rules when ext.prebid.floors.enabled = false in request"() { given: "Default BidRequest with disabled floors" def bidRequest = bidRequestWithFloors.tap { ext.prebid.floors.enabled = requestEnabled @@ -42,24 +55,45 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, SUCCESS) + when: "PBS processes auction request" - floorsPbsService.sendAuctionRequest(bidRequest) + def response = floorsPbsService.sendAuctionRequest(bidRequest) then: "Bidder request bidFloor should correspond request" - def bidderRequest = bidder.getBidderRequest(bidRequest.id) + def bidderRequest = bidder.getBidderRequests(bidRequest.id).last assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor assert !bidderRequest.ext?.prebid?.floors?.enabled and: "PBS should not fetch rules from floors provider" assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0 + and: "PBS should not add warning or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "PBS shouldn't log a errors" + def message = "Price floor rules data must be present" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert !floorsLogs.size() + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + where: requestEnabled | accountEnabled false | true true | false } - def "PBS should skip signalling for request without rules when ext.prebid.floors.enabled = false in request"() { + def "PBS should skip signaling for request without rules when ext.prebid.floors.enabled = false in request"() { given: "Default BidRequest" def bidRequest = BidRequest.getDefaultBidRequest(APP).tap { ext.prebid.floors = new ExtPrebidFloors(enabled: false) @@ -99,7 +133,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() @@ -138,7 +172,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1 } - def "PBS should not signalling when neither fetched floors nor ext.prebid.floors exist, imp.bidFloor is not defined"() { + def "PBS should not signaling when neither fetched floors nor ext.prebid.floors exist, imp.bidFloor is not defined"() { given: "Default BidRequest" def bidRequest = BidRequest.defaultBidRequest @@ -166,7 +200,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1 } - def "PBS should make PF signalling when skipRate = #skipRate"() { + def "PBS should make PF signaling when skipRate = #skipRate"() { given: "Default BidRequest with bidFloor, bidFloorCur" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].bidFloor = PBSUtils.randomFloorValue @@ -202,7 +236,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { skipRate << [0, null] } - def "PBS should not make PF signalling, enforcing when skipRate = 100"() { + def "PBS should not make PF signaling, enforcing when skipRate = 100"() { given: "Default BidRequest with bidFloor, bidFloorCur" def bidRequest = BidRequest.defaultBidRequest.tap { imp[0].bidFloor = 0.8 @@ -236,7 +270,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert bidderRequest.ext?.prebid?.floors?.skipRate == 100 assert bidderRequest.ext?.prebid?.floors?.skipped - and: "PBS should not made signalling" + and: "PBS should not made signaling" assert !bidderRequest.imp[0].ext?.prebid?.floors where: @@ -273,7 +307,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { when: "PBS processes auction request" def response = floorsPbsService.sendAuctionRequest(bidRequest) - then: "PBS should not log warning" + then: "PBS should not log warning or errors" assert !response.ext.warnings assert !response.ext.errors @@ -312,8 +346,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { when: "PBS processes amp request" def response = floorsPbsService.sendAmpRequest(ampRequest) - then: "PBS should not log warning" + then: "PBS should not log warning or errors" assert !response.ext.warnings + assert !response.ext.errors and: "Bidder request should contain bidFloor from the request" def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) @@ -349,15 +384,14 @@ 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() - verifyAll(bidderRequest) { - imp[0].bidFloor == floorsProviderFloorValue / bidAdjustment - imp[0].ext.prebid.floors.floorRuleValue == floorsProviderFloorValue - imp[0].ext.prebid.floors.floorValue == imp[0].bidFloor - } + assert bidderRequest.imp[0].bidFloor == floorsProviderFloorValue / bidAdjustment + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors where: pbsConfigBidAdjustmentFlag | requestBidAdjustmentFlag | accountBidAdjustmentFlag @@ -369,7 +403,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 +423,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,18 +434,21 @@ 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() assert bidderRequest.imp[0].bidFloor == floorsProviderFloorValue - assert bidderRequest.imp[0].ext.prebid.floors.floorRuleValue == floorsProviderFloorValue - assert bidderRequest.imp[0].ext.prebid.floors.floorValue == floorsProviderFloorValue + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors 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"() { @@ -441,8 +482,9 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { then: "Bidder request bidFloor should be update according to bidAdjustment" def bidderRequest = bidder.getBidderRequests(bidRequest.id).last() assert bidderRequest.imp[0].bidFloor == getAdjustedValue(floorValue, bidAdjustment) - assert bidderRequest.imp[0].ext.prebid.floors.floorRuleValue == floorValue - assert bidderRequest.imp[0].ext.prebid.floors.floorValue == bidderRequest.imp[0].bidFloor + + and: "Bidder request shouldn't include imp.ext.prebid.floors" + assert !bidderRequest.imp[0].ext.prebid.floors } def "PBS should remove non-selected models"() { @@ -459,7 +501,7 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { - modelGroups << ModelGroup.modelGroup + modelGroups << FloorModelGroup.modelGroup modelGroups.first().values = [(rule): floorValue + 0.1] modelGroups.last().schema = new PriceFloorSchema(fields: [SITE_DOMAIN]) modelGroups.last().values = [(new Rule(siteDomain: domain).rule): floorValue] @@ -505,4 +547,607 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { assert bidderRequest.imp.first().bidFloor == bannerFloorValue assert bidderRequest.imp.last().bidFloor == videoFloorValue } + + def "PBS shouldn't emit warning when request schema.fields equal to floor-config.max-schema-dims"() { + given: "Bid request with schema 2 fields" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.maxSchemaDims = PBSUtils.getRandomNumber(2) + } + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId) + accountDao.save(account) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should not add warning or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "Alerts.general metrics shouldn't be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert !metrics[ALERT_GENERAL] + } + + def "PBS should emit warning when request has more rules than price-floor.max-rules"() { + given: "BidRequest with 2 rules" + def requestFloorValue = PBSUtils.randomFloorValue + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups[0].values = + [(rule) : requestFloorValue + 0.1, + (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue] + } + + and: "Account with maxRules and disabled fetching in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidResponse.seatbid.first().bid.first().price = requestFloorValue + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor rules number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + maxRules | maxRulesSnakeCase + MAX_RULES_SIZE | null + null | MAX_RULES_SIZE + } + + def "PBS should emit warning when request has more schema.fields than floor-config.max-schema-dims"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims and disabled fetching in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + MAX_SCHEMA_DIMENSIONS_SIZE | null + null | MAX_SCHEMA_DIMENSIONS_SIZE + } + + def "PBS should emit warning when request has more schema.fields than default-account.max-schema-dims"() { + given: "Floor config with default account" + def accountConfig = getDefaultAccountConfigSettings().tap { + auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + } + def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true", + "settings.default-account-config": encode(accountConfig)] + + and: "Prebid server with floor config" + def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig) + + and: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + + config.auction.priceFloors.maxSchemaDims = PBSUtils.randomNegativeNumber + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Metrics should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + assert metrics[ALERT_GENERAL] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsFloorConfig) + } + + def "PBS should emit warning when request has more schema.fields than default-account.fetch.max-schema-dims"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Floor config with default account" + def accountConfig = getDefaultAccountConfigSettings().tap { + auction.priceFloors.fetch.enabled = false + auction.priceFloors.fetch.url = BASIC_FETCH_URL + bidRequest.site.publisher.id + auction.priceFloors.fetch.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + } + def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled" : "true", + "settings.default-account-config": encode(accountConfig)] + + and: "Prebid server with floor config" + def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.maxSchemaDims = PBSUtils.randomNegativeNumber + } + accountDao.save(account) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "Response should includer error warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS shouldn't log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Metrics should be updated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1 + assert metrics[ALERT_GENERAL] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsFloorConfig) + } + + def "PBS should emit warning when request has more schema.fields than fetch.max-schema-dims"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with disabled fetch in the DB" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.maxSchemaDims = maxSchemaDims + config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + maxSchemaDims | maxSchemaDimsSnakeCase + MAX_SCHEMA_DIMENSIONS_SIZE | null + null | MAX_SCHEMA_DIMENSIONS_SIZE + } + + def "PBS should fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims and disabled fetching in the DB" + def accountId = bidRequest.site.publisher.id + def floorSchemaFilesSize = getSchemaSize(bidRequest) + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE + config.auction.priceFloors.fetch.maxSchemaDims = floorSchemaFilesSize + } + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor schema dimensions ${floorSchemaFilesSize} " + + "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS shouldn't fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() { + given: "BidRequest with schema 2 fields" + def bidRequest = bidRequestWithFloors + + and: "Account with maxSchemaDims in the DB" + def accountId = bidRequest.site.publisher.id + def account = getAccountWithEnabledFetch(accountId).tap { + config.auction.priceFloors.maxSchemaDims = getSchemaSize(bidRequest) + config.auction.priceFloors.fetch.maxSchemaDims = getSchemaSize(bidRequest) - 1 + } + accountDao.save(account) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't add warnings or errors" + assert !response.ext?.warnings + } + + def "PBS should emit warning when stored request has more rules than price-floor.max-rules for amp request"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default stored request with 2 rules" + def requestFloorValue = PBSUtils.randomFloorValue + def ampStoredRequest = BidRequest.defaultStoredRequest.tap { + ext.prebid.floors = ExtPrebidFloors.extPrebidFloors + ext.prebid.floors.data.modelGroups[0].values = + [(rule) : requestFloorValue + 0.1, + (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue] + } + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account with maxRules and disabled fetching in the DB" + def account = getAccountWithEnabledFetch(ampRequest.account as String).tap { + config.auction.priceFloors.fetch.enabled = false + config.auction.priceFloors.maxRules = maxRules + config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase + } + accountDao.save(account) + + and: "Set bidder response" + def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest) + bidResponse.seatbid.first().bid.first().price = requestFloorValue + bidder.setResponse(ampStoredRequest.id, bidResponse) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes amp request" + def response = floorsPbsService.sendAmpRequest(ampRequest) + + then: "PBS should log a warning" + def message = "Price floor rules number ${getRuleSize(ampStoredRequest)} " + + "exceeded its maximum number ${MAX_RULES_SIZE}" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, "Price Floors can't be resolved for account $ampRequest.account and " + + "request $ampStoredRequest.id, reason: Price floors processing failed: Fetching is disabled. " + + "Following parsing of request price floors is failed: $message") + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + maxRules | maxRulesSnakeCase + MAX_RULES_SIZE | null + null | MAX_RULES_SIZE + } + + def "PBS should emit error in log and response when floors skipRate is out of range"() { + given: "BidRequest with invalid skipRate" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.skipRate = requestSkipRate + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + then: "PBS should log a warning" + def message = "Price floor data skipRate must be in range(0-100), but was $requestSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestSkipRate << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)] + } + + def "PBS should emit error in log and response when floors modelGroups is empty"() { + given: "BidRequest with empty modelGroups" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = requestModelGroups + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor rules should contain at least one model group" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestModelGroups << [null, []] + } + + def "PBS should emit error in log and response when modelGroup modelWeight is out of range"() { + given: "BidRequest with invalid modelWeight" + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new FloorModelGroup(modelWeight: requestModelWeight) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup modelWeight must be in range(1-100), but was $requestModelWeight" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + + where: + requestModelWeight << [PBSUtils.randomNegativeNumber, PBSUtils.getRandomNumber(100)] + } + + def "PBS should emit error in log and response when modelGroup skipRate is out of range"() { + given: "BidRequest with invalid modelGroup skipRate" + def requestModelGroupsSkipRate = PBSUtils.getRandomNumber(100) + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new FloorModelGroup(skipRate: requestModelGroupsSkipRate) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup skipRate must be in range(0-100), but was $requestModelGroupsSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log an errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS should emit error in log and response when modelGroup defaultFloor is negative"() { + given: "BidRequest with negative defaultFloor" + def requestModelGroupsSkipRate = PBSUtils.randomNegativeNumber + + def bidRequest = bidRequestWithFloors.tap { + ext.prebid.floors.data.modelGroups = [ + new FloorModelGroup(defaultFloor: requestModelGroupsSkipRate) + ] + } + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + and: "Default bid response" + def response = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, response) + + when: "PBS processes auction request" + def bidResponse = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor modelGroup default must be positive float, but was $requestModelGroupsSkipRate" + assert bidResponse.ext?.warnings[PREBID]*.code == [999] + assert bidResponse.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, FETCHING_DISABLED_ERROR, message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + def "PBS should emit error in log and response when account have disabled dynamic data config"() { + given: "Bid request without floors" + def bidRequest = getBidRequestWithFloors().tap { + ext.prebid.floors.data = null + } + + and: "Account with disabled dynamic data" + def account = getAccountWithEnabledFetch(bidRequest.accountId).tap { + config.auction.priceFloors.useDynamicData = false + } + accountDao.save(account) + + and: "PBS fetch rules from floors provider" + cacheFloorsProviderRules(bidRequest, floorsPbsService, GENERIC, SUCCESS) + + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Flush metrics" + flushMetrics(floorsPbsService) + + when: "PBS processes auction request" + def response = floorsPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should log a warning" + def message = "Price floor rules data must be present" + assert response.ext?.warnings[PREBID]*.code == [999] + assert response.ext?.warnings[PREBID]*.message == [WARNING_MESSAGE(message)] + + and: "PBS should log a errors" + def logs = floorsPbsService.getLogsByTime(startTime) + def floorsLogs = getLogsByText(logs, PRICE_FLOORS_ERROR_LOG(bidRequest, 'Using dynamic data is not allowed', message)) + assert floorsLogs.size() == 1 + + and: "Alerts.general metrics should be populated" + def metrics = floorsPbsService.sendCollectedMetricsRequest() + assert metrics[ALERT_GENERAL] == 1 + } + + private static int getSchemaSize(BidRequest bidRequest) { + bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].schema.fields.size() + } + + private static int getRuleSize(BidRequest bidRequest) { + bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].values.size() + } + + private static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { + floorValue.divide(bidAdjustment, FLOOR_VALUE_PRECISION, RoundingMode.HALF_UP) + } } 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..d9563b7fe4d 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,27 +9,35 @@ 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.v1.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.GppSectionId.US_NAT_V1 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_TID import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD import static org.prebid.server.functional.model.request.auction.Condition.ConditionType.BIDDER +import static org.prebid.server.functional.model.request.auction.PrivacyModule.ALL +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.BASIC import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE +import static org.prebid.server.functional.model.response.auction.RuleResult.ABSTAIN +import static org.prebid.server.functional.model.response.auction.RuleResult.ALLOW +import static org.prebid.server.functional.model.response.auction.RuleResult.DISALLOW import static org.prebid.server.functional.util.privacy.model.State.ALABAMA import static org.prebid.server.functional.util.privacy.model.State.ARIZONA @@ -41,6 +49,9 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { private static final def PROCESSING_ACTIVITY_TRACE = ["Processing rule."] + private final static Integer MIN_PERCENT_AB = 0 + private final static Integer MAX_PERCENT_AB = 100 + def "PBS auction shouldn't log info about activity in response when ext.prebid.trace=null"() { given: "Default basic generic BidRequest" def accountId = PBSUtils.randomNumber as String @@ -93,10 +104,10 @@ 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") + assert fetchBidsActivity.result.contains(DISALLOW) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } } @@ -120,7 +131,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - def bidResponse = pbsServiceFactory.getService(PBS_CONFIG).sendAuctionRequest(bidRequest) + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) then: "Bid response should contain basic info in debug" def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure @@ -133,10 +144,10 @@ 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") + assert fetchBidsActivity.result.contains(ALLOW) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -148,7 +159,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 +174,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 +189,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 +203,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 +238,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, @@ -236,7 +247,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { gpc: gpc, geoCodes: [new GeoCode(country: CAN, region: ARIZONA.abbreviation)])) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) - assert fetchBidsActivity.result.contains("ABSTAIN") + assert fetchBidsActivity.result.contains(ABSTAIN) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -265,7 +276,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 +309,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) } @@ -342,7 +353,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { assert fetchBidsActivity.ruleConfiguration.contains(new RuleConfiguration( and: [new And(and: ["USNatDefault. Precomputed result: ABSTAIN."])])) assert fetchBidsActivity.allowByDefault.contains(activity.defaultAction) - assert fetchBidsActivity.result.contains("ABSTAIN") + assert fetchBidsActivity.result.contains(ABSTAIN) assert fetchBidsActivity.country.every { it == null } assert fetchBidsActivity.region.every { it == null } @@ -397,11 +408,359 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec { allow << [false, true] } - private List getActivityByName(List activityInfrastructures, - ActivityType activity) { + def "PBS auction should log info about activity in response when ext.prebid.trace=verbose and skipRate=#skipRate"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: skipRate) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = 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 + } + + and: "Bid response should contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it.contains(DISALLOW.toString()) } + + and: "Should not contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + !it.privacyModule + !it.skipped + !it.result + } + + where: + skipRate << [null, MIN_PERCENT_AB] + } + + def "PBS auction should log info about module skip in response when ext.prebid.trace=verbose and skipRate is max"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [ALL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: code, enabled: true, skipRate: MAX_PERCENT_AB) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Bid response should not contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it == null } + + and: "Should contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + it.privacyModule == code + it.skipped == true + it.result == ABSTAIN + } + + where: + code << [IAB_US_GENERAL, IAB_US_CUSTOM_LOGIC] + } + + def "PBS auction should log consistently for each activity about skips modules in response"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = new AllowActivities( + syncUser: activity, + fetchBids: activity, + enrichUfpd: activity, + reportAnalytics: activity, + transmitUfpd: activity, + transmitEids: activity, + transmitPreciseGeo: activity, + transmitTid: activity, + ) + + and: "Account gpp configuration" + def skipRate = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB) + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: skipRate) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Bid response should log consistently for each activity about skips" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def fetchBidsLogs = findProcessingRule(infrastructure, FETCH_BIDS).ruleConfiguration.and + def transmitUfpdLogs = findProcessingRule(infrastructure, TRANSMIT_UFPD).ruleConfiguration.and + def transmitEidsLogs = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + def transmitPreciseGeoLogs = findProcessingRule(infrastructure, TRANSMIT_PRECISE_GEO).ruleConfiguration.and + def transmitTidLogs = findProcessingRule(infrastructure, TRANSMIT_TID).ruleConfiguration.and + verifyAll ([fetchBidsLogs, transmitUfpdLogs, transmitEidsLogs, transmitPreciseGeoLogs, transmitTidLogs]) { + it.privacyModule.toSet().size() == 1 + it.skipped.toSet().size() == 1 + it.result.toSet().size() == 1 + } + } + + def "PBS auction shouldn't emit errors or warnings when skip rate is at minimum boundary"() { + given: "A bid request with verbose tracing and GPC disallow logic" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "An activity rule with GPP SID and privacy regulation setup" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account GPP configuration with minimum skip rate" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: Integer.MIN_VALUE) + + and: "Save the account with configured activities and privacy module" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors or warnings" + assert !bidResponse.ext?.errors + assert !bidResponse.ext?.warnings + + and: "Bid response should contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it.contains(DISALLOW.toString()) } + + and: "Should not contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + !it.privacyModule + !it.skipped + !it.result + } + + and: "Generic bidder request should have empty EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + !genericBidderRequest.user.eids + !genericBidderRequest.user?.ext?.eids + } + } + + def "PBS auction shouldn't emit errors or warnings when skip rate is at maximum boundary"() { + given: "A bid request with verbose tracing and GPC disallow logic" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = VERBOSE + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "An activity rule with GPP SID and privacy regulation setup" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account GPP configuration with maximum skip rate" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: Integer.MAX_VALUE) + + and: "Save the account with configured activities and privacy module" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes the auction request" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Response should not contain errors or warnings" + assert !bidResponse.ext?.errors + assert !bidResponse.ext?.warnings + + and: "Bid response should not contain info about triggered activity in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + def ruleConfigurations = findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration.and + assert ruleConfigurations.size() == 1 + assert ruleConfigurations.first.and.every { it == null } + + and: "Should contain information that module was skipped" + verifyAll(ruleConfigurations.first) { + it.privacyModule == IAB_US_GENERAL + it.skipped == true + it.result == ABSTAIN + } + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + } + + def "PBS auction shouldn't log info about module skip in response when ext.prebid.trace=basic and skipRate is max"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = BASIC + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [ALL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: MAX_PERCENT_AB) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Bid response should not contain info about rule configuration in debug" + def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure + assert !findProcessingRule(infrastructure, TRANSMIT_EIDS).ruleConfiguration + } + + def "PBS auction shouldn't log info about module skip in response when ext.prebid.trace=null and skipRate is max"() { + given: "Default bid request" + def accountId = PBSUtils.randomNumber as String + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + ext.prebid.trace = null + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Set up activities" + def condition = Condition.baseCondition.tap { + it.gppSid = [US_NAT_V1.intValue] + } + def activityRule = ActivityRule.getDefaultActivityRule(condition).tap { + it.privacyRegulation = [ALL] + } + def activity = Activity.getDefaultActivity([activityRule]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, activity) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, skipRate: MAX_PERCENT_AB) + + and: "Save account with allow activities setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction requests" + def bidResponse = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Bid response should not contain info about trace" + assert !bidResponse.ext.debug.trace + } + + private static List getActivityByName(List activityInfrastructures, + ActivityType activity) { def firstIndex = activityInfrastructures.findLastIndexOf { it -> it.activity == activity } def lastIndex = activityInfrastructures.findIndexOf { it -> it.activity == activity } activityInfrastructures[new IntRange(true, firstIndex, lastIndex)] } -} + private static ActivityInfrastructure findProcessingRule(List infrastructures, ActivityType activity) { + def matchingActivities = getActivityByName(infrastructures, activity) + .findAll { PROCESSING_ACTIVITY_TRACE.contains(it.description) } + + if (matchingActivities.size() != 1) { + throw new IllegalStateException("Expected a single processing activity, but found ${matchingActivities.size()}") + } + + matchingActivities.first() + } +} 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..c2bf06fe50a 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 @@ -1,19 +1,30 @@ package org.prebid.server.functional.tests.privacy +import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountDsaConfig 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.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 +37,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 +54,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 +66,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 +96,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 +109,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 +136,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 +146,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 +171,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 +192,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 +229,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 +260,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 +286,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" @@ -310,18 +317,17 @@ class DsaSpec extends PrivacyBaseSpec { assert response.ext.seatnonbid.size() == 1 def seatNonBid = response.ext.seatnonbid[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == BidderName.GENERIC 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 +335,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 +355,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 +381,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 +400,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 +427,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 +451,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 +464,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 == BidderName.GENERIC + 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 == BidderName.GENERIC + 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..717c9f32d5b 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,14 +1,18 @@ 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.pricefloors.Country import org.prebid.server.functional.model.request.auction.BidRequest -import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.container.PrebidServerContainer +import org.prebid.server.functional.model.request.auction.DistributionChannel +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.BogusConsent import org.prebid.server.functional.util.privacy.CcpaConsent @@ -19,15 +23,31 @@ 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.pricefloors.Country.BULGARIA +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.request.auction.PublicCountryIp.BGR_IP 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 +277,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)] } @@ -297,10 +318,8 @@ class GdprAmpSpec extends PrivacyBaseSpec { def startTime = Instant.now() and: "Create new container" - def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG + - ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]) - serverContainer.start() - def privacyPbsService = new PrebidServerService(serverContainer) + def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String] + def defaultPrivacyPbsService = pbsServiceFactory.getService(config) and: "Prepare tcf consent string" def tcfConsent = new TcfConsent.Builder() @@ -324,31 +343,32 @@ class GdprAmpSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion) when: "PBS processes amp request" - privacyPbsService.sendAmpRequest(ampRequest) + defaultPrivacyPbsService.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" - PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } - def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Created new TCF 2 vendor list for version " + "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}") - cleanup: "Stop container with default request" - serverContainer.stop() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) 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 +381,481 @@ 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[ALERT_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: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "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" + defaultPrivacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + def vendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !defaultPrivacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = defaultPrivacyPbsService.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" + defaultPrivacyPbsService.sendAmpRequest(ampRequest) + + then: "PBS shouldn't fetch vendor list" + assert !defaultPrivacyPbsService.isFileExist(vendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + 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: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "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" + defaultPrivacyPbsService.sendAmpRequest(ampRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Amp default request" + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Bid request with gdpr and coppa config" + def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap { + regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa)) + setAccountId(ampRequest.account) + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA])) + def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest) + + then: "Bidder shouldn't be called" + 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 + + where: + gdpr | coppa | extGdpr | extCoppa + 1 | 1 | 1 | 1 + 1 | 1 | 1 | 0 + 1 | 1 | 1 | null + 1 | 1 | 0 | 1 + 1 | 1 | 0 | 0 + 1 | 1 | 0 | null + 1 | 1 | null | 1 + 1 | 1 | null | 0 + 1 | 1 | null | null + 1 | 0 | 1 | 1 + 1 | 0 | 1 | 0 + 1 | 0 | 1 | null + 1 | 0 | 0 | 1 + 1 | 0 | 0 | 0 + 1 | 0 | 0 | null + 1 | 0 | null | 1 + 1 | 0 | null | 0 + 1 | 0 | null | null + 1 | null | 1 | 1 + 1 | null | 1 | 0 + 1 | null | 1 | null + 1 | null | 0 | 1 + 1 | null | 0 | 0 + 1 | null | 0 | null + 1 | null | null | 1 + 1 | null | null | 0 + 1 | null | null | null + + null | 1 | 1 | 1 + null | 1 | 1 | 0 + null | 1 | 1 | null + null | 0 | 1 | 1 + null | 0 | 1 | 0 + null | 0 | 1 | null + null | null | 1 | 1 + null | null | 1 | 0 + null | null | 1 | null + } + + def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Amp default request" + def ampRequest = getGdprAmpRequest(validConsentString) + + and: "Bid request with gdpr and coppa config" + def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap { + regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1)) + device.geo.country = requestCountry + device.geo.region = null + device.ip = requestIpV4 + device.ipv6 = requestIpV6 + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry) + def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig) + accountDao.save(account) + + and: "Stored request in DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes amp request" + privacyPbsService.sendAmpRequest(ampRequest, header) + + then: "Bidder shouldn't be called" + 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 + + where: + requestCountry | accountCountry | requestIpV4 | requestIpV6 | header + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | BGR_IP.v4 | null | [:] + BULGARIA | null | BGR_IP.v4 | null | [:] + BULGARIA | BULGARIA | null | BGR_IP.v6 | [:] + BULGARIA | null | null | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | null | null | [:] + BULGARIA | null | null | null | [:] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + null | null | BGR_IP.v4 | BGR_IP.v6 | [:] + null | BULGARIA | BGR_IP.v4 | null | [:] + null | null | BGR_IP.v4 | null | [:] + null | BULGARIA | null | BGR_IP.v6 | [:] + null | null | null | BGR_IP.v6 | [:] + null | BULGARIA | null | null | [:] + null | null | null | null | [:] } } 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..299d911a398 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,11 +1,17 @@ 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.AccountMetricsConfig +import org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel +import org.prebid.server.functional.model.config.PurposeConfig +import org.prebid.server.functional.model.config.PurposeEnforcement +import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.Regs +import org.prebid.server.functional.model.request.auction.RegsExt 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 import org.prebid.server.functional.util.privacy.BogusConsent import org.prebid.server.functional.util.privacy.TcfConsent @@ -17,12 +23,34 @@ 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.AccountMetricsVerbosityLevel.DETAILED +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.PublicCountryIp.BGR_IP +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 { @@ -239,9 +267,9 @@ class GdprAuctionSpec extends PrivacyBaseSpec { assert seatNonBids.size() == 1 def seatNonBid = seatNonBids[0] - assert seatNonBid.seat == GENERIC.value + assert seatNonBid.seat == GENERIC 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() @@ -252,10 +280,8 @@ class GdprAuctionSpec extends PrivacyBaseSpec { def startTime = Instant.now() and: "Create new container" - def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG + - ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]) - serverContainer.start() - def privacyPbsService = new PrebidServerService(serverContainer) + def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String] + def defaultPrivacyPbsService = pbsServiceFactory.getService(config) and: "Tcf consent setup" def tcfConsent = new TcfConsent.Builder() @@ -272,31 +298,32 @@ class GdprAuctionSpec extends PrivacyBaseSpec { vendorListResponse.setResponse(tcfPolicyVersion) when: "PBS processes auction request" - privacyPbsService.sendAuctionRequest(bidRequest) + defaultPrivacyPbsService.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" - PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) } - def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion and: "Logs should contain proper vendor list version" - def logs = privacyPbsService.getLogsByTime(startTime) + def logs = defaultPrivacyPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Created new TCF 2 vendor list for version " + "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}") - cleanup: "Stop container with default request" - serverContainer.stop() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(config) 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 +338,765 @@ 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[ALERT_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: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + and: "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" + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + assert !defaultPrivacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logs = defaultPrivacyPbsService.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" + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) + + then: "PBS shouldn't fetch vendor list" + assert !defaultPrivacyPbsService.isFileExist(properVendorListPath) + + and: "Logs should contain proper vendor list version" + def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime) + assert getLogsByText(logsSecond, tcfError) + + and: "Reset vendor list response" + vendorListResponse.reset() + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + 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: "Prebid server with privacy settings" + def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG) + + 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: "Set vendor list response" + vendorListResponse.setResponse(tcfPolicyVersion) + + when: "PBS processes auction request" + defaultPrivacyPbsService.sendAuctionRequest(bidRequest) + + then: "Used vendor list have proper specification version of GVL" + def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString()) + PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) } + def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class) + assert vendorList.gvlSpecificationVersion == V3 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG) + + where: + tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5] + } + + def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request with gdpr and coppa config" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa)) + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA])) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder shouldn't be called" + 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_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: + gdpr | coppa | extGdpr | extCoppa + 1 | 1 | 1 | 1 + 1 | 1 | 1 | 0 + 1 | 1 | 1 | null + 1 | 1 | 0 | 1 + 1 | 1 | 0 | 0 + 1 | 1 | 0 | null + 1 | 1 | null | 1 + 1 | 1 | null | 0 + 1 | 1 | null | null + 1 | 0 | 1 | 1 + 1 | 0 | 1 | 0 + 1 | 0 | 1 | null + 1 | 0 | 0 | 1 + 1 | 0 | 0 | 0 + 1 | 0 | 0 | null + 1 | 0 | null | 1 + 1 | 0 | null | 0 + 1 | 0 | null | null + 1 | null | 1 | 1 + 1 | null | 1 | 0 + 1 | null | 1 | null + 1 | null | 0 | 1 + 1 | null | 0 | 0 + 1 | null | 0 | null + 1 | null | null | 1 + 1 | null | null | 0 + 1 | null | null | null + + null | 1 | 1 | 1 + null | 1 | 1 | 0 + null | 1 | 1 | null + null | 0 | 1 | 1 + null | 0 | 1 | 0 + null | 0 | 1 | null + null | null | 1 | 1 + null | null | 1 | 0 + null | null | 1 | null + } + + def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() { + given: "Valid consent string without basic ads" + def validConsentString = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + + and: "Bid request with gdpr and coppa config" + def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap { + regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1)) + device.geo.country = requestCountry + device.geo.region = null + device.ip = requestIpV4 + device.ipv6 = requestIpV6 + } + + and: "Save account config without eea countries into DB" + def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry) + def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(privacyPbsService) + + when: "PBS processes auction request" + privacyPbsService.sendAuctionRequest(bidRequest, header) + + then: "Bidder shouldn't be called" + 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_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1 + + where: + requestCountry | accountCountry | requestIpV4 | requestIpV6 | header + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | BGR_IP.v4 | null | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | BGR_IP.v6 | ["X-Forwarded-For": BGR_IP.v4] + null | BULGARIA | null | null | ["X-Forwarded-For": BGR_IP.v4] + null | null | null | null | ["X-Forwarded-For": BGR_IP.v4] + BULGARIA | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | null | BGR_IP.v4 | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | BGR_IP.v4 | null | [:] + BULGARIA | null | BGR_IP.v4 | null | [:] + BULGARIA | BULGARIA | null | BGR_IP.v6 | [:] + BULGARIA | null | null | BGR_IP.v6 | [:] + BULGARIA | BULGARIA | null | null | [:] + BULGARIA | null | null | null | [:] + null | BULGARIA | BGR_IP.v4 | BGR_IP.v6 | [:] + null | null | BGR_IP.v4 | BGR_IP.v6 | [:] + null | BULGARIA | BGR_IP.v4 | null | [:] + null | null | BGR_IP.v4 | null | [:] + null | BULGARIA | null | BGR_IP.v6 | [:] + null | null | null | BGR_IP.v6 | [:] + null | BULGARIA | null | null | [:] + null | null | null | null | [:] + } + + def "PBS auction shouldn't update buyeruid scrubbed metrics when user.buyeruid not requested"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.buyeruid = null + user.ext.consent = new TcfConsent.Builder().build() + 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).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: verbosityLevel) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed shouldn't be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert !metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + assert !metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + + where: + verbosityLevel << [DETAILED, AccountMetricsVerbosityLevel.BASIC] + } + + def "PBS auction should update buyeruid scrubbed general metrics when user.buyeruid requested and verbosityLevel BASIC"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = new TcfConsent.Builder().build() + 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).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: AccountMetricsVerbosityLevel.BASIC) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 + + and: "Account metric shouldn't be populated" + assert !metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] + } + + def "PBS auction should update buyeruid scrubbed general and account metrics when user.buyeruid requested and verbosityLevel DETAILED"() { + given: "Default bid requests with personal data" + def bidRequest = bidRequestWithPersonalData.tap { + regs.gdpr = 1 + user.ext.consent = new TcfConsent.Builder().build() + 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).tap { + config.metrics = new AccountMetricsConfig(verbosityLevel: DETAILED) + } + accountDao.save(account) + + and: "Flush metric" + flushMetrics(privacyPbsService) + + when: "PBS processes auction requests" + privacyPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should mask user personal data" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll(bidderRequest.user) { + !id + !buyeruid + !yob + !gender + !eids + !data + !geo + !ext + !eids + !ext?.eids + } + + and: "Metrics buyeruid scrubbed should be updated" + def metrics = privacyPbsService.sendCollectedMetricsRequest() + assert metrics["adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 + assert metrics["account.${account.uuid}.adapter.${GENERIC.value}.requests.buyeruid_scrubbed"] == 1 } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy new file mode 100644 index 00000000000..df46f493775 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy @@ -0,0 +1,337 @@ +package org.prebid.server.functional.tests.privacy + +import org.prebid.server.functional.model.UidsCookie +import org.prebid.server.functional.model.config.AccountAuctionConfig +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.request.setuid.SetuidRequest +import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo +import org.prebid.server.functional.service.PrebidServerException +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils +import org.prebid.server.functional.util.privacy.TcfConsent +import org.prebid.server.util.ResourceUtil + +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.bidder.BidderName.GENER_X +import static org.prebid.server.functional.model.config.Purpose.P1 +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.request.setuid.UidWithExpiry.getDefaultUidWithExpiry +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 +import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS + +class GdprSetUidSpec extends PrivacyBaseSpec { + + private static final boolean CORS_SUPPORT = false + private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT + private static final Map VENDOR_GENERIC_PBS_CONFIG = GENERIC_VENDOR_CONFIG + + ["gdpr.purposes.p1.enforce-purpose" : NO.value, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved" + private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451 + + private static final PrebidServerService prebidServerService = pbsServiceFactory.getService(VENDOR_GENERIC_PBS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(VENDOR_GENERIC_PBS_CONFIG) + } + + def "PBS setuid shouldn't failed with tcf when purpose access device not enforced"() { + given: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain tempUids cookie and headers" + assert response.headers.size() == 7 + assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + } + + def "PBS setuid shouldn't failed with tcf when bidder name and cookie-family-name mismatching"() { + given: "PBS with different cookie-family-name" + def pbsConfig = VENDOR_GENERIC_PBS_CONFIG + + ["adapters.${GENERIC.value}.usersync.cookie-family-name": GENER_X.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENER_X + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with gener_x bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENER_X): defaultUidWithExpiry] + } + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain tempUids cookie and headers" + assert response.headers.size() == 7 + assert response.uidsCookie.tempUIDs[GENER_X].uid == setuidRequest.uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS setuid should failed with tcf when dgpr value is invalid"() { + given: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([PBSUtils.getRandomNumberWithExclusion(GENERIC_VENDOR_ID, 0, 65534)]) + .build() + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE + assert exception.responseBody == TCF_ERROR_MESSAGE + + and: "Metric should be increased usersync.FAMILY.tcf.blocked" + def metric = prebidServerService.sendCollectedMetricsRequest() + assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1 + } + + def "PBS setuid should failed with tcf when purpose access device enforced for account"() { + given: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: FULL)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE + assert exception.responseBody == TCF_ERROR_MESSAGE + + and: "Metric should be increased usersync.FAMILY.tcf.blocked" + def metric = prebidServerService.sendCollectedMetricsRequest() + assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1 + } + + def "PBS setuid should failed with tcf when purpose access device enforced for host"() { + given: "PBS config" + def pbsConfig = VENDOR_GENERIC_PBS_CONFIG + ["gdpr.purposes.p1.enforce-purpose": FULL.value] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Request should fail with error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE + assert exception.responseBody == TCF_ERROR_MESSAGE + + and: "Metric should be increased usersync.FAMILY.tcf.blocked" + def metric = prebidServerService.sendCollectedMetricsRequest() + assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS setuid shouldn't failed with tcf when purpose access device not enforced for host and host-vendor-id empty"() { + given: "PBS config" + def pbsConfig = VENDOR_GENERIC_PBS_CONFIG + ["gdpr.purposes.p1.enforce-purpose": NO.value, + "gdpr.host-vendor-id" : ""] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account config with purpose into DB" + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain tempUids cookie and headers" + assert response.headers.size() == 7 + assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS setuid shouldn't failed with purpose access device enforced for account when bidder included in vendorExceptions"() { + given: "Default setuid request with account" + def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { + it.account = PBSUtils.randomNumber.toString() + it.uid = UUID.randomUUID().toString() + it.bidder = GENERIC + it.gdpr = "1" + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + } + + and: "Default uids cookie with generic bidder" + def uidsCookie = UidsCookie.defaultUidsCookie.tap { + it.tempUIDs = [(GENERIC): defaultUidWithExpiry] + } + + and: "Save account config with purpose into DB" + def purposeConfig = new PurposeConfig(enforcePurpose: FULL, vendorExceptions: [GENERIC.value]) + def accountConfig = new AccountConfig( + auction: new AccountAuctionConfig(debugAllow: true), + privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): purposeConfig], enabled: true))) + def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig) + accountDao.save(account) + + when: "PBS processes setuid request" + def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain tempUids cookie and headers" + assert response.headers.size() == 7 + assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid + assert response.responseBody == + ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png") + } +} 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..ab4d46117ab 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.v2.TcfEuV2Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UspV1Consent 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"() { @@ -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..d0282d48ddd 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.v2.TcfEuV2Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UspV1Consent import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 @@ -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..045dd33a784 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 @@ -1,6 +1,12 @@ package org.prebid.server.functional.tests.privacy import io.netty.handler.codec.http.HttpResponseStatus +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.request.GppSectionId import org.prebid.server.functional.model.request.cookiesync.CookieSyncRequest import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo import org.prebid.server.functional.service.PrebidServerException @@ -10,10 +16,14 @@ import org.prebid.server.functional.util.HttpUtil 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.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v2.TcfEuV2Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UspV1Consent +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.config.Purpose.P1 +import static org.prebid.server.functional.model.config.PurposeEnforcement.NO import static org.prebid.server.functional.model.request.GppSectionId.TCF_EU_V2 import static org.prebid.server.functional.model.request.GppSectionId.USP_V1 import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.IFRAME @@ -28,12 +38,27 @@ class GppCookieSyncSpec extends BaseSpec { private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT private static final boolean CORS_SUPPORT = false private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync" + private static final GppSectionId FIRST_GPP_SECTION = PBSUtils.getRandomEnum(GppSectionId.class) + private static final GppSectionId SECOND_GPP_SECTION = PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION]) + private static final Map GENERIC_CONFIG = [ "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + private static final Map GENERIC_WITH_SKIP_CONFIG = [ + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), + "adapters.${GENERIC.value}.usersync.skipwhen.gdpr" : 'true', + "adapters.${GENERIC.value}.usersync.skipwhen.gpp_sid" : "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}".toString(), + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + + private static PrebidServerService prebidServerService = pbsServiceFactory.getService(GENERIC_CONFIG) + private static PrebidServerService prebidServerServiceWithSkipConfig = pbsServiceFactory.getService(GENERIC_WITH_SKIP_CONFIG + GENERIC_ALIAS_CONFIG) - private PrebidServerService prebidServerService = pbsServiceFactory.getService(GENERIC_CONFIG) + def cleanupSpec() { + pbsServiceFactory.removeContainer(GENERIC_CONFIG) + pbsServiceFactory.removeContainer(GENERIC_WITH_SKIP_CONFIG + GENERIC_ALIAS_CONFIG) + } def "PBS cookie sync request should set GDPR to 1 when gpp_sid contains 2"() { given: "Request without GDPR and GPP SID" @@ -167,8 +192,8 @@ class GppCookieSyncSpec extends BaseSpec { it.gpp = new TcfEuV2Consent.Builder().build() it.gdpr = null it.gdprConsent = new TcfConsent.Builder().setPurposesLITransparency(DEVICE_ACCESS) - .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) - .build() + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() } when: "PBS processes cookie sync request" @@ -204,9 +229,9 @@ class GppCookieSyncSpec extends BaseSpec { def "PBS should return empty gpp and gppSid in usersync url when gpp and gppSid is not present in request"() { given: "Pbs config with usersync.#userSyncFormat.url" - def prebidServerService = pbsServiceFactory.getService( - ["adapters.generic.usersync.${userSyncFormat.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), - "adapters.generic.usersync.${userSyncFormat.value}.support-cors": "false"]) + def pbsConfig = ["adapters.generic.usersync.${userSyncFormat.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), + "adapters.generic.usersync.${userSyncFormat.value}.support-cors": "false"] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Default CookieSyncRequest without gpp and gppSid" def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { @@ -222,15 +247,18 @@ class GppCookieSyncSpec extends BaseSpec { assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp").isEmpty() assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid").isEmpty() + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: userSyncFormat << [REDIRECT, IFRAME] } def "PBS should populate gpp and gppSid in usersync url when gpp and gppSid is present in request"() { given: "Pbs config with usersync.#userSyncFormat.url" - def prebidServerService = pbsServiceFactory.getService( - ["adapters.generic.usersync.${userSyncFormat.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), - "adapters.generic.usersync.${userSyncFormat.value}.support-cors": "false"]) + def pbsConfig = ["adapters.generic.usersync.${userSyncFormat.value}.url" : "$networkServiceContainer.rootUri/generic-usersync&redir={{redirect_url}}".toString(), + "adapters.generic.usersync.${userSyncFormat.value}.support-cors": "false"] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Default CookieSyncRequest with gpp and gppSid" def gpp = PBSUtils.randomString @@ -248,7 +276,244 @@ class GppCookieSyncSpec extends BaseSpec { assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp") == gpp assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid") == gppSid + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + where: userSyncFormat << [REDIRECT, IFRAME] } + + def "PBS should emit proper error message when request contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + } + + def "PBS should emit proper error message when alias request contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.bidders = [ALIAS] + it.gppSid = TCF_EU_V2.intValue + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + } + + def "PBS should emit proper error message when request contain gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gpp = new UsNatV1Consent.Builder().build() + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + + where: + gppSid << ["${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}, ${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + ] + } + + def "PBS should emit proper error message when alias request contain gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.bidders = [ALIAS] + it.gpp = new UsNatV1Consent.Builder().build() + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url shouldn't contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert !bidderStatus.userSync + assert !bidderStatus.noCookie + + and: "Response should contain proper error message" + assert bidderStatus.error == "Rejected by regulation scope" + + where: + gppSid << ["${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${SECOND_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${SECOND_GPP_SECTION.value}, ${FIRST_GPP_SECTION.value}", + "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}, ${SECOND_GPP_SECTION.value}", + "${FIRST_GPP_SECTION.value}, ${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + ] + } + + def "PBS shouldn't emit error message when request doesn't contain gdpr config and global skip gdpr config for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def gppSid = TCF_EU_V2 + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = gppSid.intValue + it.gdpr = 0 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url should contain cookies and userSync" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp") == "" + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid") == gppSid.value + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS shouldn't emit error message when request does contain gdpr config and global skip gdpr config disabled for adapter"() { + given: "Pbs config with usersync.#userSyncFormat.url" + def pbsConfig = [ + "adapters.${GENERIC.value}.meta-info.vendor-id" : GENERIC_VENDOR_ID as String, + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL, + "adapters.${GENERIC.value}.usersync.skipwhen.gdpr" : 'false', + "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.value + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + it.account = PBSUtils.randomNumber + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def privacyConfig = new AccountPrivacyConfig(gdpr: accountGdprConfig) + def account = new Account(uuid: cookieSyncRequest.account, config: new AccountConfig(privacy: privacyConfig)) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain proper userSync url" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.userSync?.url == USER_SYNC_URL + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't emit error message when request does contain gdpr config and global skip gdpr config default for adapter"() { + given: "Default CookieSyncRequest with gdpr config" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.value + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder() + .setPurposesLITransparency(DEVICE_ACCESS) + .setVendorLegitimateInterest([GENERIC_VENDOR_ID]) + .build() + it.account = PBSUtils.randomNumber + } + + and: "Save account config with requireConsent into DB" + def purposes = [(P1): new PurposeConfig(enforcePurpose: NO, enforceVendors: false)] + def accountGdprConfig = new AccountGdprConfig(purposes: purposes) + def privacyConfig = new AccountPrivacyConfig(gdpr: accountGdprConfig) + def account = new Account(uuid: cookieSyncRequest.account, config: new AccountConfig(privacy: privacyConfig)) + accountDao.save(account) + + when: "PBS processes cookie sync request" + def response = prebidServerService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain proper userSync url" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.userSync?.url == USER_SYNC_URL + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS shouldn't emit error message when request doesn't contain matched gpp config and specific global skip gpp config for adapter"() { + given: "Default CookieSyncRequest with gpp and gppSid" + def gpp = new UsNatV1Consent.Builder().build() + def gppSid = "${PBSUtils.getRandomEnum(GppSectionId.class, [FIRST_GPP_SECTION, SECOND_GPP_SECTION]).value}" + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = TCF_EU_V2.intValue + it.gpp = gpp + it.gppSid = gppSid + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response userSync url should contain gpp and gppSid" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp") == gpp.toString() + assert HttpUtil.findUrlParameterValue(bidderStatus.userSync?.url, "gpp_sid") == gppSid + + and: "Response shouldn't contains any error" + assert !bidderStatus.error + } + + def "PBS should also include validation warning when request matches skip config and has validation issue at same time"() { + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = PBSUtils.getRandomNumberWithExclusion(TCF_EU_V2.intValue) + it.gdpr = 1 + it.gdprConsent = new TcfConsent.Builder().build() + } + + when: "PBS processes cookie sync request" + def response = prebidServerServiceWithSkipConfig.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain a warning" + assert response.warnings == ["GPP scope does not match TCF2 scope"] + + then: "Privacy for bidder should be enforced" + def bidderStatus = response.getBidderUserSync(GENERIC) + assert bidderStatus.error == "Rejected by regulation scope" + } } 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..d292bd9e6f1 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,44 +3,45 @@ 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.privacy.gpp.MspaMode +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2SensitiveData +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.data.UsCaliforniaSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v2.UsNatV2Consent 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.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 -import static org.prebid.server.functional.model.config.DataActivity.NOT_APPLICABLE -import static org.prebid.server.functional.model.config.DataActivity.NO_CONSENT +import static org.prebid.server.functional.model.config.ConfigCase.CAMEL_CASE +import static org.prebid.server.functional.model.config.ConfigCase.KEBAB_CASE +import static org.prebid.server.functional.model.config.ConfigCase.SNAKE_CASE import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.AND import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR 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 +54,23 @@ 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.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +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.privacy.gpp.GppDataActivity.CONSENT +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NOT_APPLICABLE +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NO_CONSENT 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 +79,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 +100,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 +133,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 +167,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 +179,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 +192,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 +209,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 +226,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 +252,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 +284,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 +303,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 +321,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 +376,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 +397,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 +422,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 +444,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 +467,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 +505,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 +541,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 +559,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 +581,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,15 +600,12 @@ 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" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } - + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.getDefaultActivity([rule])) and: "Account gpp configuration" @@ -642,13 +655,98 @@ 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 request have disallow logic US nat v2 validation should call bid adapter"() { + given: "Default Generic BidRequests with gppConsent and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = BidRequest.defaultBidRequest.tap { + it.setAccountId(accountId) + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = gppConsent + } + + and: "Activities set for fetchBid with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, 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 should be called" + assert bidder.getBidderRequest(bidRequest.id) + + where: + gppConsent << [ + new UsNatV2Consent.Builder().setSensitiveDataProcessing( + new UsNationalV2SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT, + consumerHealthData: CONSENT, + crimeVictim: CONSENT, + nationalOrigin: CONSENT, + transgenderStatus: CONSENT + )), + new UsNatV2Consent.Builder().setSensitiveDataProcessing( + new UsNationalV2SensitiveData( + racialEthnicOrigin: NO_CONSENT, + religiousBeliefs: NO_CONSENT, + healthInfo: NO_CONSENT, + orientation: NO_CONSENT, + citizenshipStatus: NO_CONSENT, + unionMembership: NO_CONSENT, + consumerHealthData: NO_CONSENT, + nationalOrigin: NO_CONSENT + )), + new UsNatV2Consent.Builder().setSensitiveDataProcessing( + new UsNationalV2SensitiveData( + geneticId: NO_CONSENT, + biometricId: NO_CONSENT, + idNumbers: NO_CONSENT, + accountInfo: NO_CONSENT, + communicationContents: NO_CONSENT, + crimeVictim: NO_CONSENT, + transgenderStatus: NO_CONSENT + )), + new UsNatV2Consent.Builder().setSensitiveDataProcessing( + new UsNationalV2SensitiveData( + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + communicationContents: CONSENT, + crimeVictim: CONSENT, + transgenderStatus: CONSENT + )), + new UsNatV2Consent.Builder().setKnownChildSensitiveDataConsents( + new UsNationalV2ChildSensitiveData(childFrom16to17: NO_CONSENT)) + ] } def "PBS auction call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -657,7 +755,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 +768,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" @@ -686,14 +781,22 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 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 +824,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) } @@ -736,10 +839,12 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.getDefaultActivity([rule])) and: "Account gpp configuration with sid skip" + def activityConfig = new ActivityConfig([FETCH_BIDS], LogicalRestrictedRule.generateSingleRestrictedRule(operator, rulesList)) + def accountLogic = GppModuleConfig.getDefaultModuleConfig(activityConfig, [US_NAT_V1], false, caseType) 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 +852,34 @@ 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)]) + gpcValue | operator | caseType | rulesList + false | OR | CAMEL_CASE | [new EqualityValueRule(GPC, NO_CONSENT)] + true | OR | CAMEL_CASE | [new InequalityValueRule(GPC, NO_CONSENT)] + true | AND | CAMEL_CASE | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)] + + false | OR | KEBAB_CASE | [new EqualityValueRule(GPC, NO_CONSENT)] + true | OR | KEBAB_CASE | [new InequalityValueRule(GPC, NO_CONSENT)] + true | AND | KEBAB_CASE | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)] + + false | OR | SNAKE_CASE | [new EqualityValueRule(GPC, NO_CONSENT)] + true | OR | SNAKE_CASE | [new InequalityValueRule(GPC, NO_CONSENT)] + true | AND | SNAKE_CASE | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)] } 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 +895,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 +903,32 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS auction call when custom privacy regulation empty and normalize is disabled should process request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" + 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 +944,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,24 +955,26 @@ 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) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Generic bidder should be called due to positive allow in activities" + assert bidder.getBidderRequest(bidRequest.id) and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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() + regs.gpp = gppStateConsent setAccountId(accountId) } @@ -879,81 +999,130 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } def "PBS amp call when bidder allowed in activities should process bid request and proper metrics and update processed metrics"() { @@ -990,7 +1159,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 +1197,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 +1393,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 +1436,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,15 +1450,13 @@ 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 } and: "Activities set for fetchBid with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(FETCH_BIDS, Activity.getDefaultActivity([rule])) @@ -1354,13 +1521,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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CT_V1 } def "PBS amp call when privacy regulation have duplicate should process request and update alerts metrics"() { @@ -1373,7 +1540,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 +1554,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]) @@ -1416,8 +1583,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 +1622,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 } @@ -1492,10 +1659,10 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } def "PBS amp call when privacy regulation match custom requirement should ignore call to bidder"() { @@ -1508,7 +1675,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 +1709,25 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS amp call when custom privacy regulation empty and normalize is disabled should process request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 +1735,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 +1749,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" @@ -1596,15 +1766,16 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp requests" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Generic bidder should be called" + assert bidder.getBidderRequests(ampStoredRequest.id) and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should ignore call to bidder"() { @@ -1618,7 +1789,7 @@ class GppFetchBidActivitiesSpec extends PrivacyBaseSpec { def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = gppSid.intValue - it.consentString = gppStateConsent.build() + it.consentString = gppStateConsent it.consentType = GPP } @@ -1656,74 +1827,123 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } } 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..d49565222d2 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 @@ -5,11 +5,11 @@ import org.prebid.server.functional.model.request.GppSectionId 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.v2.TcfEuV2Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UspV1Consent 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 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..87d23a3d740 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,21 @@ 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.privacy.gpp.MspaMode +import org.prebid.server.functional.model.privacy.gpp.Notice +import org.prebid.server.functional.model.privacy.gpp.OptOut +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2ChildSensitiveData 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,29 +24,23 @@ 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.data.UsCaliforniaSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v2.UsNatV2Consent import java.time.Instant -import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST 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 -import static org.prebid.server.functional.model.config.DataActivity.NOT_APPLICABLE -import static org.prebid.server.functional.model.config.DataActivity.NO_CONSENT import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.AND import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR 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 +55,42 @@ 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.PROCESSED_ACTIVITY_RULES_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.privacy.gpp.GppDataActivity.CONSENT +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NOT_APPLICABLE +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NO_CONSENT +import static org.prebid.server.functional.model.privacy.gpp.UsNationalV1ChildSensitiveData.getDefault 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,12 @@ 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 +129,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 +144,13 @@ 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 +294,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,22 +336,53 @@ 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 } + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + + 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: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + + def "PBS cookie sync call should exclude bidder URLs when privacy module contains disallowed GPP rules"() { + 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 = [privacyAllowRegulations] + it.privacyRegulation = [IAB_US_GENERAL] } def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) @@ -358,14 +401,71 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert !response.bidderStatus.userSync.url where: - privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + disallowGppLogic << [SIMPLE_GPC_DISALLOW_LOGIC, + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build()] } - def "PBS cookie sync call when privacy module contain some part of disallow logic should exclude bidders URLs"() { + def "PBS cookie sync call should exclude bidders URLs when privacy module contain opt out of disallow GPP logic"() { 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 } @@ -391,25 +491,142 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert !response.bidderStatus.userSync.url 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() - ] + disallowGppLogic << [new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.DID_NOT_OPT_OUT) + .build()] + } + + def "PBS cookie sync call should exclude bidders URLs when privacy module contain disallow child sensitive data logic US nat v2 validation"() { + 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 = new UsNatV2Consent.Builder() + .setKnownChildSensitiveDataConsents(usNationalV2ChildSensitiveData) + .build() + } + + 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: + usNationalV2ChildSensitiveData << [new UsNationalV2ChildSensitiveData(childUnder13: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom13to16: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom16to17: NO_CONSENT)] + } + + def "PBS cookie sync call should respond with required bidder URL and emit error log when privacy module contain invalid GPP segment"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 = INVALID_GPP_STRING + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response shouldn't contain warnings" + assert !response.warnings + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: SYNC_USER. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS cookie sync call when privacy module contain invalid GPP string should respond with required bidder URL and emit warning in response"() { + given: "Cookie sync request with link to account" + def accountId = PBSUtils.randomString + def invalidGpp = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = invalidGpp + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + and: "Should add a warning when in debug mode" + assert response.warnings.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) } def "PBS cookie sync call when request have different gpp consent but match and rejecting should exclude bidders URLs"() { @@ -442,20 +659,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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).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 +698,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 +706,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 } @@ -509,21 +726,34 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes cookie sync request" def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) then: "Response should contain bidders userSync.urls" assert response.getBidderUserSync(GENERIC).userSync.url + and: "Response shouldn't contain warnings" + assert !response.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(cookieSyncRequest, SYNC_USER)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", 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 +765,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" @@ -560,7 +790,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.gpp = SIMPLE_GPC_DISALLOW_LOGIC it.account = accountId } @@ -588,10 +818,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 } @@ -621,17 +851,17 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } def "PBS cookie sync when privacy regulation match custom requirement should exclude bidders URLs"() { 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 +891,25 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } 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() + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" + 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 +925,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" @@ -703,17 +936,18 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendCookieSyncRequest(cookieSyncRequest) + def response = activityPbsService.sendCookieSyncRequest(cookieSyncRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should contain bidders userSync.urls" + assert response.getBidderUserSync(GENERIC).userSync.url and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, 'USCustomLogic creation failed: objects must have exactly 1 key defined, found 0').size() == 1 } def "PBS cookie sync when custom privacy regulation with normalizing should exclude bidders URLs"() { @@ -722,7 +956,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { it.gppSid = gppSid.intValue it.account = accountId - it.gpp = gppStateConsent.build() + it.gpp = gppStateConsent } and: "Activities set for transmit ufpd with allowing privacy regulation" @@ -752,75 +986,124 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } def "PBS setuid request when bidder allowed in activities should respond with valid bidders UIDs cookies and update processed metrics"() { @@ -851,7 +1134,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 +1168,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 +1336,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,16 +1387,52 @@ 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 } and: "UIDS Cookie" def uidsCookie = UidsCookie.defaultUidsCookie + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + + 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: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + + def "PBS setuid request should reject bidders with status code invalidStatusCode when privacy module contains disallowed GPP rules"() { + 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 = [privacyAllowRegulations] + it.privacyRegulation = [IAB_US_GENERAL] } def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) @@ -1134,15 +1453,72 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE where: - privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + disallowGppLogic << [SIMPLE_GPC_DISALLOW_LOGIC, + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build()] } - def "PBS setuid request when privacy module contain some part of disallow logic should reject bidders with status code invalidStatusCode"() { + def "PBS setuid request should reject bidders with status code invalidStatusCode when privacy module contain opt out of disallow GPP UsNat v2 logic"() { given: "Cookie sync SetuidRequest with accountId" 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 } @@ -1172,25 +1548,106 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.responseBody == INVALID_STATUS_MESSAGE 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() - ] + disallowGppLogic << [new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.DID_NOT_OPT_OUT) + .build()] + } + + def "PBS setuid request when privacy module contain invalid GPP segment should respond with valid bidders UIDs cookies"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 = INVALID_GPP_STRING + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain uids cookie" + assert response.uidsCookie + assert response.responseBody + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: SYNC_USER. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS setuid request when privacy module contain invalid GPP string should respond with valid bidders UIDs cookies"() { + 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 = PBSUtils.randomString + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes cookie sync request" + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + + then: "Response should contain uids cookie" + assert response.uidsCookie + assert response.responseBody + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 } def "PBS setuid request when request have different gpp consent but match and rejecting should reject bidders with status code invalidStatusCode"() { @@ -1228,13 +1685,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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CT_V1 } def "PBS setuid request when privacy modules contain allowing settings should respond with valid bidders UIDs cookies"() { @@ -1242,7 +1699,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,8 +1727,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 +1736,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 } @@ -1301,6 +1757,9 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes cookie sync request" def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) @@ -1308,8 +1767,15 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert response.uidsCookie assert response.responseBody + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(setuidRequest, SYNC_USER)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", 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 +1783,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 +1797,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" @@ -1357,7 +1823,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 +1854,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 } @@ -1424,17 +1890,17 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } def "PBS setuid call when privacy regulation match custom requirement should reject bidders with status code invalidStatusCode"() { 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 +1935,27 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS setuid call when custom privacy regulation empty and normalize is disabled should respond with required UIDs cookies"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 +1972,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" @@ -1514,17 +1983,18 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes setuid request" - activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) + def response = activityPbsService.sendSetUidRequest(setuidRequest, uidsCookie) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should contain uids cookie" + assert response.responseBody and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, 'USCustomLogic creation failed: objects must have exactly 1 key defined, found 0').size() == 1 } def "PBS setuid call when custom privacy regulation with normalizing should reject bidders with status code invalidStatusCode"() { @@ -1533,7 +2003,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { def setuidRequest = SetuidRequest.defaultSetuidRequest.tap { it.gppSid = gppSid.intValue it.account = accountId - it.gpp = gppStateConsent.build() + it.gpp = gppStateConsent } and: "UIDs cookies" @@ -1568,82 +2038,131 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } def "PBS cookie sync should process rule when geo doesn't intersection"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + - ["geolocation.configurations.geo-info.[0].country": countyConfig, - "geolocation.configurations.geo-info.[0].region" : regionConfig]) + def pbsConfig = GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.geo-info.[0].country": countyConfig, + "geolocation.configurations.geo-info.[0].region" : regionConfig] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Cookie sync request with account connection" def accountId = PBSUtils.randomNumber as String @@ -1680,22 +2199,25 @@ 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) 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"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + - ["geolocation.configurations.[0].geo-info.country": countyConfig, - "geolocation.configurations.[0].geo-info.region" : regionConfig]) + def pbsConfig = GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, + "geolocation.configurations.[0].geo-info.region" : regionConfig] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Default set uid request" def accountId = PBSUtils.randomString @@ -1736,21 +2258,24 @@ 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) 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"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + - ["geolocation.configurations.[0].geo-info.country": countyConfig, - "geolocation.configurations.[0].geo-info.region" : regionConfig]) + def pbsConfig = GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, + "geolocation.configurations.[0].geo-info.region" : regionConfig] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Cookie sync request with account connection" def accountId = PBSUtils.randomNumber as String @@ -1788,20 +2313,23 @@ 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) 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"() { given: "Pbs config with geo location" - def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION + - ["geolocation.configurations.[0].geo-info.country": countyConfig, - "geolocation.configurations.[0].geo-info.region" : regionConfig]) + def pbsConfig = GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": countyConfig, + "geolocation.configurations.[0].geo-info.region" : regionConfig] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) and: "Default set uid request" def accountId = PBSUtils.randomString @@ -1841,9 +2369,195 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec { assert exception.statusCode == INVALID_STATUS_CODE assert exception.responseBody == INVALID_STATUS_MESSAGE + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + 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 pbsConfig = GENERAL_PRIVACY_CONFIG + GEO_LOCATION + ["geolocation.configurations.[0].geo-info.country": USA.ISOAlpha3, + "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + 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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS cookie sync should exclude bidders URLs when privacy regulation match and personal data consent is 2"() { + 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig().tap { + it.enabled = true + it.code = IAB_US_GENERAL + } + + 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: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + + def "PBS cookie sync should exclude bidders URLs when privacy regulation match and personal data consent is 2 and allowPersonalDataConsent2 is false"() { + given: "Cookie sync request with gpp privacy" + def accountId = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulation]) + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig().tap { + enabled = true + code = IAB_US_GENERAL + config = gppModuleConfig + } + + and: "Save 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: + privacyAllowRegulation | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + } + + def "PBS cookie sync shouldn't exclude bidders URLs when privacy regulation match and personal data consent is 2 and allowPersonalDataConsent2 is true"() { + given: "Cookie sync request with gpp privacy" + def accountId = PBSUtils.randomString + def cookieSyncRequest = CookieSyncRequest.defaultCookieSyncRequest.tap { + it.gppSid = US_NAT_V1.value + it.account = accountId + it.gpp = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for cookie sync with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulation]) + def activities = AllowActivities.getDefaultAllowActivities(SYNC_USER, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig().tap { + enabled = true + code = IAB_US_GENERAL + config = gppModuleConfig + } + + and: "Save 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 contain any URLs for bidders" + assert response.bidderStatus.userSync.url + where: - countyConfig | regionConfig | conditionGeo - USA.value | null | [USA.value] - USA.value | ALABAMA.abbreviation | [USA.withState(ALABAMA)] + privacyAllowRegulation | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) } } 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..1db77ef0b90 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,48 +3,42 @@ 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.privacy.gpp.MspaMode +import org.prebid.server.functional.model.privacy.gpp.Notice +import org.prebid.server.functional.model.privacy.gpp.OptOut +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1SensitiveData 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.data.UsCaliforniaSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v2.UsNatV2Consent 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 -import static org.prebid.server.functional.model.config.DataActivity.NOT_APPLICABLE -import static org.prebid.server.functional.model.config.DataActivity.NO_CONSENT import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.AND import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR 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 +53,21 @@ 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.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +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.privacy.gpp.GppDataActivity.CONSENT +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NOT_APPLICABLE +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NO_CONSENT 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 @@ -74,25 +76,16 @@ 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.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA 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,27 @@ 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 +125,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 +137,20 @@ 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 +161,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 +178,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,12 +189,11 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) - assert getLogsByText(logs, "Activity configuration for account ${accountId} " + - "contains conditional rule with empty array").size() == 1 + assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 where: conditions | isAllowed @@ -208,7 +206,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 +221,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 +246,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 +260,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 +283,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 +303,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 +326,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 +338,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 +379,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 +393,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 +430,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 +444,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 +475,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 +519,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 +559,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,23 +602,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 } 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 } and: "Activities set for transmitEIDS with rejecting privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) @@ -632,10 +628,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 @@ -646,11 +642,11 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } - def "PBS auction call when privacy module contain some part of disallow logic should remove EIDS fields in request"() { + def "PBS auction call should remove EIDS fields in request when privacy module contains disallowed GPP rules"() { 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,95 +665,285 @@ 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: - 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( - racialEthnicOrigin: 1, - religiousBeliefs: 1, - healthInfo: 1, - orientation: 1, - citizenshipStatus: 1, - unionMembership: 1, - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 1, - biometricId: 1, - idNumbers: 1, - accountInfo: 1, - communicationContents: 1 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - communicationContents: 2 - )).build() - ] + disallowGppLogic << [SIMPLE_GPC_DISALLOW_LOGIC, + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: NO_CONSENT, + religiousBeliefs: NO_CONSENT, + healthInfo: NO_CONSENT, + orientation: NO_CONSENT, + citizenshipStatus: NO_CONSENT, + unionMembership: NO_CONSENT,)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: NO_CONSENT, + biometricId: NO_CONSENT, + idNumbers: NO_CONSENT, + accountInfo: NO_CONSENT, + communicationContents: NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + communicationContents: CONSENT)) + .build()] + } + + def "PBS auction call should remove EIDS fields in request when privacy module contain opt out of disallow GPP UsNat v2 logic"() { + 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 << [new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.OPTED_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build()] + } + + def "PBS auction call shouldn't remove EIDS fields in request and emit error log when privacy module contain invalid GPP segment"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 = INVALID_GPP_STRING + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_EIDS. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS auction call when privacy module contain invalid GPP string shouldn't remove EIDS fields in request and emit warning in response"() { + given: "Default Generic BidRequests with EIDS fields and account id" + def accountId = PBSUtils.randomNumber as String + def invalidGpp = PBSUtils.randomString + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = invalidGpp + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + 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 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 +963,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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).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 +1002,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 } @@ -851,22 +1037,39 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) accountDao.save(account) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + def response = 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: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_EIDS)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] where: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + regsGpp << [null, "", 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,18 +1083,18 @@ 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() @@ -901,8 +1104,8 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { 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 +1123,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 +1133,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,25 +1158,25 @@ 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 - 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } 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 +1199,36 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS auction call when custom privacy regulation empty and normalize is disabled should leave EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" + 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 +1243,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,25 +1254,34 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(bidRequest.id) + assert genericBidderRequest.user.eids[0].source == bidRequest.user.eids[0].source + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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() + regs.gpp = gppStateConsent } and: "Activities set with privacy regulation" @@ -1090,93 +1305,142 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1204,15 +1468,15 @@ 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1244,16 +1508,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_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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1288,9 +1552,9 @@ 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1310,10 +1574,9 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) - assert getLogsByText(logs, "Activity configuration for account ${accountId} " + - "contains conditional rule with empty array").size() == 1 + assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 where: conditions | isAllowed @@ -1326,9 +1589,9 @@ 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1360,9 +1623,9 @@ 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1397,11 +1660,11 @@ 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1438,16 +1701,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_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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1481,26 +1744,24 @@ 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" + and: "Default 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 } and: "Activities set for transmitEIDS with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) @@ -1529,15 +1790,15 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } - def "PBS amp call when privacy module contain some part of disallow logic should remove EIDS fields in request"() { + def "PBS amp call should remove EIDS fields in request when privacy module contains disallowed GPP rules"() { 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" + and: "Default 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 } @@ -1571,87 +1832,303 @@ 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( - racialEthnicOrigin: 1, - religiousBeliefs: 1, - healthInfo: 1, - orientation: 1, - citizenshipStatus: 1, - unionMembership: 1, - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 1, - biometricId: 1, - idNumbers: 1, - accountInfo: 1, - communicationContents: 1 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - communicationContents: 2 - )).build() - ] + disallowGppLogic << [SIMPLE_GPC_DISALLOW_LOGIC, + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: NO_CONSENT, + religiousBeliefs: NO_CONSENT, + healthInfo: NO_CONSENT, + orientation: NO_CONSENT, + citizenshipStatus: NO_CONSENT, + unionMembership: NO_CONSENT,)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: NO_CONSENT, + biometricId: NO_CONSENT, + idNumbers: NO_CONSENT, + accountInfo: NO_CONSENT, + communicationContents: NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + communicationContents: CONSENT)) + .build()] + } + + def "PBS amp call should remove EIDS fields in request when privacy module contain opt out of disallow GPP UsNat v2 logic"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default 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 << [new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.OPTED_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build()] + } + + def "PBS amp call when privacy module contain invalid GPP segment shouldn't remove EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + + "Response should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_EIDS. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS amp call when privacy module contain invalid GPP string shouldn't remove EIDS fields in request and emit warning in response"() { + given: "Default Generic BidRequest with EIDS fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def invalidGpp = PBSUtils.randomString + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = invalidGpp + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] } 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = gppSid.value @@ -1688,24 +2165,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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).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" + and: "Default 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 +2212,77 @@ 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 empty in request 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 = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = regsGpp + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes amp request" + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_EIDS)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + + where: + regsGpp << [null, ""] + } + 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" + and: "Default 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 } @@ -1771,25 +2306,31 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) then: "Generic bidder request should have data in EIDS fields" def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + assert genericBidderRequest.user.eids == ampStoredRequest.user.eids + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $regsGpp"] 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" + and: "Default 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 +2346,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]) @@ -1830,12 +2371,12 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { 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" + and: "Default 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 +2410,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 } @@ -1910,21 +2451,21 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } 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 +2503,29 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS amp call when custom privacy regulation empty and normalize is disabled should leave EIDS fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 +2541,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" @@ -2012,29 +2556,37 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp requests" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string error" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $gppConsent"] and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in EIDS fields" + def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert genericBidderRequest.user.eids[0].source == ampStoredRequest.user.eids[0].source + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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 { it.account = accountId it.gppSid = gppSid.intValue - it.consentString = gppStateConsent.build() + it.consentString = gppStateConsent it.consentType = GPP } @@ -2076,84 +2628,242 @@ class GppTransmitEidsActivitiesSpec extends PrivacyBaseSpec { } where: - gppSid | equalityValueRules | gppStateConsent - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } - 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]) + def "PBS should remove EIDS fields in request when privacy regulation match and personalDataConsents is 2"() { + given: "Default bid requests 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + 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: "Bidder request shouldn't contain EIDS fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + assert !bidderRequest.user?.ext?.eids + + where: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + + def "PBS should remove EIDS fields in request when privacy regulation match and personalDataConsents is 2 and allowPersonalDataConsent2 is false"() { + given: "Default bid requests 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(enabled: true, code: IAB_US_GENERAL, config: gppModuleConfig) + + 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: "Bidder request shouldn't contain EIDS fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert !bidderRequest.user.eids + assert !bidderRequest.user?.ext?.eids + + where: + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + } + + def "PBS shouldn't remove EIDS fields in request when privacy regulation match and personalDataConsents is 2 and allowPersonalDataConsent2 is true"() { + given: "Default bid requests 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitEIDS with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_EIDS, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(enabled: true, code: IAB_US_GENERAL, config: gppModuleConfig) + + and: "Existed account with privacy regulation setup" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities, [accountGppConfig]) + accountDao.save(account) + + when: "PBS processes auction request" + activityPbsService.sendAuctionRequest(bidRequest) + + then: "Bidder request should contain EIDS fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.user.eids + + where: + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) } } 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..e3be1b13e98 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,43 +3,41 @@ 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.privacy.gpp.MspaMode +import org.prebid.server.functional.model.privacy.gpp.Notice +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1SensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2ChildSensitiveData 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.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.data.UsCaliforniaSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v2.UsNatV2Consent 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 -import static org.prebid.server.functional.model.config.DataActivity.NOT_APPLICABLE -import static org.prebid.server.functional.model.config.DataActivity.NO_CONSENT import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.AND import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR 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 +52,34 @@ 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.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +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.privacy.gpp.GppDataActivity.CONSENT +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NOT_APPLICABLE +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NO_CONSENT 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 +88,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 +100,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 +144,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 +156,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 +223,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 +302,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 +350,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 +410,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 +476,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 +547,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 +589,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 +600,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 +619,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 +655,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 +685,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 +731,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 +757,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 +793,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 +824,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 +861,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 +887,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 +922,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 +948,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,14 +987,12 @@ 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 } - and: "Activities set for transmitPreciseGeINTERNAL_SERVER_ERRORo with rejecting privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + and: "Activities set for transmitPreciseGeo with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.getDefaultActivity([rule])) @@ -790,26 +1008,43 @@ 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: privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } - def "PBS auction call when privacy module contain some part of disallow logic should round lat/lon data to 2 digits"() { + def "PBS auction call should round lat/lon data to 2 digits when privacy module contains disallowed GPP rules"() { given: "Default Generic BidRequests with gppConsent and account id" 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,45 +1067,141 @@ 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() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 1 - )).build() + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: NO_CONSENT)) + .build() + ] + } + + def "PBS auction call should round lat/lon data to 2 digits when privacy module contain disallow child sensitive data logic US nat v2 validation"() { + given: "Default Generic BidRequests with gppConsent and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = bidRequestWithGeo.tap { + it.setAccountId(accountId) + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV2Consent.Builder() + .setKnownChildSensitiveDataConsents(usNationalV2ChildSensitiveData) + .build() + } + + and: "Activities set for transmitPreciseGeo with rejecting privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, 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: "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::" + 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: + usNationalV2ChildSensitiveData << [ + new UsNationalV2ChildSensitiveData(childUnder13: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom13to16: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom16to17: NO_CONSENT) ] } - def "PBS auction call when request have different gpp consent but match and rejecting should round lat/lon data to 2 digits"() { + def "PBS auction call should round lat/lon data to 2 digits when disallow gpp string match with #gppSid sid"() { given: "Default Generic BidRequests with gppConsent and account id" def accountId = PBSUtils.randomNumber as String def bidRequest = bidRequestWithGeo.tap { @@ -898,32 +1229,49 @@ 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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CT_V1 } - def "PBS auction call when privacy modules contain allowing settings should not round lat/lon data"() { + def "PBS auction call should not round lat/lon data when privacy modules contain allowing GPP settings"() { given: "Default basic generic BidRequest" 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 +1291,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 +1317,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 +1326,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 +1349,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 +1382,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 +1396,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,12 +1407,26 @@ 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 } @@ -1052,7 +1441,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 +1469,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,33 +1496,47 @@ 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 { - 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 + 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.user.geo.lat == bidRequest.user.geo.lat + bidderRequests.user.geo.lon == bidRequest.user.geo.lon } 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } 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 +1561,58 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS auction call when custom privacy regulation empty and normalize is disabled should not round lat/lon data and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" + 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 +1630,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,25 +1641,49 @@ 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) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + 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.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 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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() + regs.gpp = gppStateConsent setAccountId(accountId) } @@ -1264,90 +1711,156 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } def "PBS amp call with bidder allowed in activities should not round lat/lon data and update processed metrics"() { @@ -1382,19 +1895,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 +1955,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 +2017,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 +2121,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 +2178,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 +2243,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 +2309,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,15 +2350,13 @@ 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 } and: "Activities set for transmitPreciseGeo with allowing privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.getDefaultActivity([rule])) @@ -1758,21 +2376,38 @@ 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: privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } - def "PBS amp call when privacy module contain some part of disallow logic should round lat/lon data to 2 digits"() { + def "PBS amp call should round lat/lon data to 2 digits when privacy module contains disallowed GPP rules"() { given: "Default Generic BidRequest" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = bidRequestWithGeo @@ -1780,7 +2415,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,45 +2443,76 @@ 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() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - geolocation: 1 - )).build() + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData(geolocation: NO_CONSENT)) + .build() ] } - def "PBS amp call when request have different gpp consent but match and rejecting should round lat/lon data to 2 digits"() { + def "PBS amp call should round lat/lon data to 2 digits when disallow gpp string match with #gppSid sid"() { given: "Default Generic BidRequest" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = bidRequestWithGeo @@ -1882,27 +2548,44 @@ 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(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CT_V1 } - def "PBS amp call when privacy modules contain allowing settings should not round lat/lon data"() { + def "PBS amp call should not round lat/lon data when privacy modules contain allowing settings"() { given: "Default Generic BidRequest" def accountId = PBSUtils.randomNumber as String def ampStoredRequest = bidRequestWithGeo @@ -1910,7 +2593,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 +2618,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 +2644,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 +2656,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 +2684,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 +2719,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 +2733,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,12 +2748,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 } @@ -2062,7 +2784,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 +2819,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 +2827,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,22 +2859,35 @@ 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: 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } def "PBS amp call when privacy regulation match custom requirement should round lat/lon data to 2 digits"() { @@ -2165,7 +2900,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 +2933,51 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS amp call when custom privacy regulation empty and normalize is disabled should not round lat/lon data and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 +2985,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 +3001,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" @@ -2263,15 +3018,38 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp requests" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + 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.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 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 1 } def "PBS amp call when custom privacy regulation with normalizing should change request consent and call to bidder"() { @@ -2285,7 +3063,7 @@ class GppTransmitPreciseGeoActivitiesSpec extends PrivacyBaseSpec { def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId it.gppSid = gppSid.intValue - it.consentString = gppStateConsent.build() + it.consentString = gppStateConsent it.consentType = GPP } @@ -2321,85 +3099,342 @@ 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::" + 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 + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) + } + + def "PBS auction should round lat/lon data to 2 digits call when privacy regulation match and personalDataConsents is 2"() { + given: "Default bid requests with gppConsent and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = bidRequestWithGeo.tap { + it.setAccountId(accountId) + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitPreciseGeo with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, 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: "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::" + 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: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + def "PBS auction should round lat/lon data to 2 digits call when privacy regulation match and personalDataConsents is 2 and allowPersonalDataConsent2 is false"() { + given: "Default bid requests with gppConsent and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = bidRequestWithGeo.tap { + it.setAccountId(accountId) + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitPreciseGeo with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(enabled: true, code: IAB_US_GENERAL, config: gppModuleConfig) + + 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: "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::" - 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 == 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: + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + } + + def "PBS auction shouldn't round lat/lon data to 2 digits call when privacy regulation match and personalDataConsents is 2 and allowPersonalDataConsent2 is true"() { + given: "Default bid requests with gppConsent and account id" + def accountId = PBSUtils.randomNumber as String + def bidRequest = bidRequestWithGeo.tap { + it.setAccountId(accountId) + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitPreciseGeo with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_PRECISE_GEO, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(enabled: true, code: IAB_US_GENERAL, config: gppModuleConfig) + + 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: "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.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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) } } 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..b648a4fe91c 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,52 +4,47 @@ 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.privacy.gpp.MspaMode +import org.prebid.server.functional.model.privacy.gpp.Notice +import org.prebid.server.functional.model.privacy.gpp.OptOut +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsNationalV2SensitiveData +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.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.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.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.data.UsCaliforniaSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsNationalSensitiveData -import org.prebid.server.functional.util.privacy.gpp.data.UsUtahSensitiveData +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent +import org.prebid.server.functional.model.privacy.gpp.UsNationalV1SensitiveData +import org.prebid.server.functional.util.privacy.gpp.v2.UsNatV2Consent 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.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 -import static org.prebid.server.functional.model.config.DataActivity.NOT_APPLICABLE -import static org.prebid.server.functional.model.config.DataActivity.NO_CONSENT import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.AND import static org.prebid.server.functional.model.config.LogicalRestrictedRule.LogicalOperation.OR 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 +57,23 @@ 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.ACCOUNT_PROCESSED_RULES_COUNT +import static org.prebid.server.functional.model.privacy.Metric.PROCESSED_ACTIVITY_RULES_COUNT +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.privacy.gpp.GppDataActivity.CONSENT +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NOT_APPLICABLE +import static org.prebid.server.functional.model.privacy.gpp.GppDataActivity.NO_CONSENT 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 @@ -80,25 +82,16 @@ 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.model.response.auction.ErrorType.PREBID import static org.prebid.server.functional.util.privacy.model.State.ALABAMA 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 +101,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 +151,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 +203,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 +235,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,9 +246,9 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" + then: "Logs should contain error" def logs = activityPbsService.getLogsByTime(startTime) assert getLogsByText(logs, "Activity configuration for account ${accountId} " + "contains conditional rule with empty array").size() == 1 @@ -253,7 +264,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 +279,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 +323,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 +374,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 +413,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 +436,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 +513,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 +537,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 +579,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 +603,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 +643,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 +702,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 +761,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,29 +819,31 @@ 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 } and: "Activities set for transmitUfpd with rejecting privacy regulation" - def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] - } + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) @@ -812,36 +855,39 @@ 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] } - def "PBS auction call when privacy module contain some part of disallow logic should remove UFPD fields in request"() { + def "PBS auction call should remove UFPD fields in request when privacy module contains disallowed GPP v1 rules"() { 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,109 +906,159 @@ 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( - racialEthnicOrigin: 1, - religiousBeliefs: 1, - healthInfo: 1, - orientation: 1, - citizenshipStatus: 1, - unionMembership: 1, + new UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: NO_CONSENT, + religiousBeliefs: NO_CONSENT, + healthInfo: NO_CONSENT, + orientation: NO_CONSENT, + citizenshipStatus: NO_CONSENT, + unionMembership: NO_CONSENT, )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 1, - biometricId: 1, - idNumbers: 1, - accountInfo: 1, - communicationContents: 1 + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: NO_CONSENT, + biometricId: NO_CONSENT, + idNumbers: NO_CONSENT, + accountInfo: NO_CONSENT, + communicationContents: NO_CONSENT )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - communicationContents: 2 + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + communicationContents: CONSENT )).build() ] } - def "PBS auction call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { + def "PBS auction call should remove UFPD fields in request when privacy module contain opt out of disallow GPP UsNat v2 logic"() { given: "Default Generic BidRequests with UFPD fields and account id" def accountId = PBSUtils.randomNumber as String - def genericBidRequest = givenBidRequestWithAccountAndUfpdData(accountId).tap { - regs.gppSid = [gppSid.intValue] - regs.gpp = gppConsent + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = disallowGppLogic } and: "Activities set for transmitUfpd with rejecting privacy regulation" @@ -980,96 +1076,144 @@ 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.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 + disallowGppLogic << [ + new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.OPTED_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build() + ] } - def "PBS auction call when privacy modules contain allowing settings should leave UFPD fields in request"() { - given: "Default basic generic BidRequest" + def "PBS auction call should remove UFPD fields in request when privacy module contain any of US nat v2 sensitive data processing match"() { + 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] - regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + def gppString = new UsNatV2Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.PROVIDED) + .setSensitiveDataProcessing(usNationalV2SensitiveData) + .build() + .toString() + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = gppString } - and: "Activities set for transmitUfpd with allowing privacy regulation" + 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(genericBidRequest) - - then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + activityPbsService.sendAuctionRequest(bidRequest) - and: "Generic bidder should be called due to positive allow in activities" + then: "Generic bidder request should have empty UFPD fields" + 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 + !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: - accountGppConfig << [ - new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), - new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [USP_NAT_V1]), enabled: true) + usNationalV2SensitiveData << [ + new UsNationalV2SensitiveData(racialEthnicOrigin: CONSENT), + new UsNationalV2SensitiveData(religiousBeliefs: CONSENT), + new UsNationalV2SensitiveData(healthInfo: CONSENT), + new UsNationalV2SensitiveData(orientation: CONSENT), + new UsNationalV2SensitiveData(citizenshipStatus: CONSENT), + new UsNationalV2SensitiveData(geneticId: CONSENT), + new UsNationalV2SensitiveData(biometricId: CONSENT), + new UsNationalV2SensitiveData(idNumbers: CONSENT), + new UsNationalV2SensitiveData(accountInfo: CONSENT), + new UsNationalV2SensitiveData(unionMembership: CONSENT), + new UsNationalV2SensitiveData(communicationContents: CONSENT), + new UsNationalV2SensitiveData(consumerHealthData: CONSENT), + new UsNationalV2SensitiveData(crimeVictim: CONSENT), + new UsNationalV2SensitiveData(nationalOrigin: CONSENT), + new UsNationalV2SensitiveData(transgenderStatus: CONSENT), + + new UsNationalV2SensitiveData(racialEthnicOrigin: NO_CONSENT), + new UsNationalV2SensitiveData(religiousBeliefs: NO_CONSENT), + new UsNationalV2SensitiveData(healthInfo: NO_CONSENT), + new UsNationalV2SensitiveData(orientation: NO_CONSENT), + new UsNationalV2SensitiveData(citizenshipStatus: NO_CONSENT), + new UsNationalV2SensitiveData(unionMembership: NO_CONSENT), + new UsNationalV2SensitiveData(consumerHealthData: NO_CONSENT), + new UsNationalV2SensitiveData(nationalOrigin: NO_CONSENT), + + new UsNationalV2SensitiveData(geneticId: NO_CONSENT), + new UsNationalV2SensitiveData(biometricId: NO_CONSENT), + new UsNationalV2SensitiveData(idNumbers: NO_CONSENT), + new UsNationalV2SensitiveData(accountInfo: NO_CONSENT), + new UsNationalV2SensitiveData(communicationContents: NO_CONSENT), + new UsNationalV2SensitiveData(crimeVictim: NO_CONSENT), + new UsNationalV2SensitiveData(transgenderStatus: NO_CONSENT) ] } - def "PBS auction call when regs.gpp in request is allowing should leave UFPD fields in request"() { - given: "Default basic generic BidRequest" + def "PBS auction call should remove UFPD fields in request when privacy module contain disallow child sensitive data logic US nat v2 validation"() { + 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] - regs.gpp = regsGpp + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = new UsNatV2Consent.Builder() + .setKnownChildSensitiveDataConsents(usNationalV2ChildSensitiveData) + .build() } - and: "Activities set for transmitUfpd with allowing privacy regulation" + and: "Activities set for transmitUfpd with rejecting privacy regulation" def rule = new ActivityRule().tap { it.privacyRegulation = [IAB_US_GENERAL] } @@ -1084,81 +1228,404 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) - - then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(genericBidRequest.id) + activityPbsService.sendAuctionRequest(bidRequest) - and: "Generic bidder should be called due to positive allow in activities" + then: "Generic bidder request should have empty UFPD fields" + 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 + !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: - regsGpp << ["", new UspNatV1Consent.Builder().build(), new UspNatV1Consent.Builder().setGpc(false).build()] + usNationalV2ChildSensitiveData << [ + new UsNationalV2ChildSensitiveData(childUnder13: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom13to16: NO_CONSENT), + new UsNationalV2ChildSensitiveData(childFrom16to17: NO_CONSENT) + ] } - 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 "PBS auction call when privacy module contain invalid GPP segment shouldn't remove UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 = INVALID_GPP_STRING } - and: "Activities set for transmitUfpd with privacy regulation" - def ruleUsGeneric = new ActivityRule().tap { + 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([ruleUsGeneric])) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - and: "Flush metrics" - flushMetrics(activityPbsService) + 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: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + 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.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: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ALERT_GENERAL] == 1 + + and: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_UFPD. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 + } + + def "PBS auction call when privacy module contain invalid GPP string shouldn't remove UFPD fields in request and emit warning in response"() { + given: "Default Generic BidRequests with UFPD fields and account id" + def accountId = PBSUtils.randomNumber as String + def invalidGpp = PBSUtils.randomString + def bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = invalidGpp + } + + 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) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + verifyAll { + 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.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: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) + + and: "Response should not contain any errors" + assert !response.ext.errors + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + 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 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 bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [gppSid.intValue] + regs.gpp = gppConsent + } + + 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.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 UsNatV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).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 bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = SIMPLE_GPC_DISALLOW_LOGIC + } + + 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: "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 data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + and: "Generic bidder should be called due to positive allow in activities" + verifyAll { + 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: [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 bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + regs.gpp = regsGpp + } + + 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: "Flush metrics" + flushMetrics(activityPbsService) + + when: "PBS processes auction requests" + def response = activityPbsService.sendAuctionRequest(bidRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + and: "Generic bidder should be called due to positive allow in activities" + verifyAll { + 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: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics for disallowed activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + assert metrics[ACCOUNT_PROCESSED_RULES_COUNT.getValue(bidRequest, TRANSMIT_UFPD)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] + + where: + regsGpp << [null, "", 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 bidRequest = getBidRequestWithPersonalData(accountId).tap { + regs.gppSid = [US_NAT_V1.intValue] + } + + and: "Activities set for transmitUfpd with privacy regulation" + def ruleUsGeneric = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } + + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([ruleUsGeneric])) + + 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 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 @@ -1167,8 +1634,8 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { 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 +1653,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 +1663,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,40 +1688,43 @@ 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)]) - 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } 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 +1747,51 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS auction call when custom privacy regulation empty and normalize is disabled should leave UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Generic BidRequest with gpp and account setup" + 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 +1806,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,25 +1817,54 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { accountDao.save(account) when: "PBS processes auction requests" - activityPbsService.sendAuctionRequest(genericBidRequest) + def response = activityPbsService.sendAuctionRequest(bidRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "JsonLogic exception: objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should not contain any errors" + assert !response.ext.errors and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + + and: "Generic bidder should be called due to positive allow in activities" + verifyAll { + 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: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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() + regs.gpp = gppStateConsent } and: "Activities set with privacy regulation" @@ -1383,105 +1888,157 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } @@ -1503,43 +2060,304 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + verifyAll { + 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[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 = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Allow activities setup" + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + + and: "Flush metrics" + flushMetrics(activityPbsService) + + and: "Saved account config with allow activities into DB" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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.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[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 = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Allow activities setup" + def activity = new Activity(defaultAction: false) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + + and: "Saved account config with allow activities into DB" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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.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"() { + given: "Test start time" + def startTime = Instant.now() + + and: "Default Generic BidRequest with UFPD fields field and account id" + def accountId = PBSUtils.randomNumber as String + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Activities set with have empty condition type" + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(conditions, isAllowed)]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + + and: "Saved account config with allow activities into DB" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "Activity configuration for account ${accountId} " + + "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 + } + + 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 = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Activity rules with same priority" + def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) + def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) + + and: "Activities set for bidder allowed by hierarchy structure" + def activity = Activity.getDefaultActivity([allowActivity, disallowActivity]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + + and: "Save account config with allow activities into DB" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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 data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + + verifyAll { + 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 = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def ampRequest = AmpRequest.defaultAmpRequest.tap { + it.account = accountId + } + + and: "Activities set for actions with Generic bidder rejected by hierarchy setup" + def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) + def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) + + and: "Activities set for bidder disallowing by hierarchy structure" + def activity = Activity.getDefaultActivity([disallowActivity, allowActivity]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + + and: "Saved account config with allow activities into DB" + def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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 { - 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 + !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: "Metrics processed across activities should be updated" - def metrics = activityPbsService.sendCollectedMetricsRequest() - assert metrics[ACTIVITY_RULES_PROCESSED_COUNT] == 2 + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids } - def "PBS amp call when transmit UFPD activities is rejecting request should remove UFPD fields field in active request and update disallowed metrics"() { + 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) + def ampStoredRequest = getBidRequestWithPersonalData(accountId).tap { + it.regs.ext = new RegsExt(gpc: null) + } - and: "amp request with link to account" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } and: "Allow activities setup" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(Condition.baseCondition, false)]) + def condition = Condition.baseCondition.tap { + it.componentType = null + it.componentName = null + it.gpc = VALID_VALUE_FOR_GPC_HEADER + } + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) and: "Flush metrics" @@ -1554,47 +2372,58 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + 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 call when default activity setting set to false should remove UFPD fields from request"() { + 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId } and: "Allow activities setup" - def activity = new Activity(defaultAction: false) + def condition = Condition.baseCondition.tap { + it.componentType = null + it.componentName = null + it.gpc = PBSUtils.randomNumber as String + } + def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + and: "Flush metrics" + flushMetrics(activityPbsService) + and: "Saved account config with allow activities into DB" def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) accountDao.save(account) @@ -1604,48 +2433,59 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + 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) + then: "Generic bidder request should have data in UFPD fields" + 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 == 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 bidder allowed activities have empty condition type should skip this rule and emit an error"() { - given: "Test start time" - def startTime = Instant.now() + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + } - and: "Default Generic BidRequest with UFPD fields field and account id" + 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = SIMPLE_GPC_DISALLOW_LOGIC + it.consentType = GPP } - and: "Activities set with have empty condition type" - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(conditions, isAllowed)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) - and: "Saved account config with allow activities into DB" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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" @@ -1655,39 +2495,62 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def logs = activityPbsService.getLogsByTime(startTime) - assert getLogsByText(logs, "Activity configuration for account ${accountId} " + - "contains conditional rule with empty array").size() == 1 + 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.geo + !bidderRequest.user.ext + } + + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids where: - conditions | isAllowed - new Condition(componentType: []) | true - new Condition(componentType: []) | false - new Condition(componentName: []) | true - new Condition(componentName: []) | false + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] } - def "PBS amp call when first rule allowing in activities should leave UFPD fields in request"() { + def "PBS amp should remove UFPD fields in request when privacy module contain any of US nat v2 sensitive data processing match"() { given: "Default Generic BidRequest with UFPD fields field and account id" def accountId = PBSUtils.randomNumber as String - def ampStoredRequest = givenBidRequestWithAccountAndUfpdData(accountId) - - and: "amp request with link to account" + def ampStoredRequest = getBidRequestWithPersonalData(accountId) + + and: "Default amp request with link to account" + def gppString = new UsNatV2Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.PROVIDED) + .setSensitiveDataProcessing(usNationalV2SensitiveData) + .build() + .toString() def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = gppString + it.consentType = GPP } - and: "Activity rules with same priority" - def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) - def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } - and: "Activities set for bidder allowed by hierarchy structure" - def activity = Activity.getDefaultActivity([allowActivity, disallowActivity]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - and: "Save account config with allow activities into DB" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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" @@ -1697,47 +2560,89 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Generic bidder request should have data in UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - + then: "Generic bidder request should have empty UFPD fields" + 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 + !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: + usNationalV2SensitiveData << [ + new UsNationalV2SensitiveData(racialEthnicOrigin: CONSENT), + new UsNationalV2SensitiveData(religiousBeliefs: CONSENT), + new UsNationalV2SensitiveData(healthInfo: CONSENT), + new UsNationalV2SensitiveData(orientation: CONSENT), + new UsNationalV2SensitiveData(citizenshipStatus: CONSENT), + new UsNationalV2SensitiveData(geneticId: CONSENT), + new UsNationalV2SensitiveData(biometricId: CONSENT), + new UsNationalV2SensitiveData(idNumbers: CONSENT), + new UsNationalV2SensitiveData(accountInfo: CONSENT), + new UsNationalV2SensitiveData(unionMembership: CONSENT), + new UsNationalV2SensitiveData(communicationContents: CONSENT), + new UsNationalV2SensitiveData(consumerHealthData: CONSENT), + new UsNationalV2SensitiveData(crimeVictim: CONSENT), + new UsNationalV2SensitiveData(nationalOrigin: CONSENT), + new UsNationalV2SensitiveData(transgenderStatus: CONSENT), + + new UsNationalV2SensitiveData(racialEthnicOrigin: NO_CONSENT), + new UsNationalV2SensitiveData(religiousBeliefs: NO_CONSENT), + new UsNationalV2SensitiveData(healthInfo: NO_CONSENT), + new UsNationalV2SensitiveData(orientation: NO_CONSENT), + new UsNationalV2SensitiveData(citizenshipStatus: NO_CONSENT), + new UsNationalV2SensitiveData(unionMembership: NO_CONSENT), + new UsNationalV2SensitiveData(consumerHealthData: NO_CONSENT), + new UsNationalV2SensitiveData(nationalOrigin: NO_CONSENT), + + new UsNationalV2SensitiveData(geneticId: NO_CONSENT), + new UsNationalV2SensitiveData(biometricId: NO_CONSENT), + new UsNationalV2SensitiveData(idNumbers: NO_CONSENT), + new UsNationalV2SensitiveData(accountInfo: NO_CONSENT), + new UsNationalV2SensitiveData(communicationContents: NO_CONSENT), + new UsNationalV2SensitiveData(crimeVictim: NO_CONSENT), + new UsNationalV2SensitiveData(transgenderStatus: NO_CONSENT) + ] } - def "PBS amp call when first rule disallowing in activities should remove UFPD fields in request"() { + def "PBS amp call should remove UFPD fields in request when privacy module contains disallowed GPP rules"() { 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" + and: "Default 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 actions with Generic bidder rejected by hierarchy setup" - def disallowActivity = new ActivityRule(condition: Condition.baseCondition, allow: false) - def allowActivity = new ActivityRule(condition: Condition.baseCondition, allow: true) + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] + } - and: "Activities set for bidder disallowing by hierarchy structure" - def activity = Activity.getDefaultActivity([disallowActivity, allowActivity]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - and: "Saved account config with allow activities into DB" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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" @@ -1748,51 +2653,176 @@ 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 UsNatV1Consent.Builder() + .setMspaServiceProviderMode(MspaMode.YES) + .setMspaOptOutOptionMode(MspaMode.NO) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSaleOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingNotice(Notice.NOT_PROVIDED) + .setSharingOptOutNotice(Notice.PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .setSharingOptOut(OptOut.OPTED_OUT) + .setSharingNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOutNotice(Notice.NOT_PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setTargetedAdvertisingOptOut(OptOut.OPTED_OUT) + .setTargetedAdvertisingOptOutNotice(Notice.PROVIDED) + .setSaleOptOut(OptOut.OPTED_OUT) + .setSaleOptOutNotice(Notice.PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_PROVIDED) + .setMspaServiceProviderMode(MspaMode.NO) + .setMspaOptOutOptionMode(MspaMode.YES) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NOT_APPLICABLE, NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsNationalV1ChildSensitiveData.getDefault(NO_CONSENT, NOT_APPLICABLE)) + .build(), + new UsNatV1Consent.Builder() + .setPersonalDataConsents(CONSENT) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: NO_CONSENT, + religiousBeliefs: NO_CONSENT, + healthInfo: NO_CONSENT, + orientation: NO_CONSENT, + citizenshipStatus: NO_CONSENT, + unionMembership: NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataLimitUseNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder() + .setSensitiveDataProcessingOptOutNotice(Notice.NOT_APPLICABLE) + .setSensitiveDataProcessing(new UsNationalV1SensitiveData( + racialEthnicOrigin: CONSENT, + religiousBeliefs: CONSENT, + healthInfo: CONSENT, + orientation: CONSENT, + citizenshipStatus: CONSENT, + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + unionMembership: CONSENT, + communicationContents: CONSENT)) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: NO_CONSENT, + biometricId: NO_CONSENT, + idNumbers: NO_CONSENT, + accountInfo: NO_CONSENT, + communicationContents: NO_CONSENT)) + .build(), + new UsNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalV1SensitiveData( + geneticId: CONSENT, + biometricId: CONSENT, + idNumbers: CONSENT, + accountInfo: CONSENT, + communicationContents: CONSENT)) + .build() + ] } - def "PBS amp should disallowed rule when header.gpc intersection with condition.gpc"() { + def "PBS amp call should remove UFPD fields in request when privacy module contain opt out of disallow GPP UsNat v2 logic"() { 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) - and: "amp request with link to account" + and: "Default 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: "Allow activities setup" - def condition = Condition.baseCondition.tap { - it.componentType = null - it.componentName = null - it.gpc = VALID_VALUE_FOR_GPC_HEADER + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] } - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) - and: "Flush metrics" - flushMetrics(activityPbsService) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) - and: "Saved account config with allow activities into DB" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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" @@ -1800,107 +2830,143 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest, ["Sec-GPC": VALID_VALUE_FOR_GPC_HEADER]) + 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.ext } - 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 + and: "Generic bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == ampStoredRequest.user.eids + + where: + disallowGppLogic << [ + new UsNatV2Consent.Builder() + .setSaleOptOut(OptOut.DID_NOT_OPT_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOutNotice(Notice.NOT_PROVIDED) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.OPTED_OUT) + .build(), + new UsNatV2Consent.Builder() + .setSharingOptOut(OptOut.DID_NOT_OPT_OUT) + .build() + ] } - 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 "PBS amp call when privacy module contain invalid GPP segment shouldn't remove UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId + it.gppSid = US_NAT_V1.value + it.consentString = INVALID_GPP_STRING + it.consentType = GPP } - and: "Allow activities setup" - def condition = Condition.baseCondition.tap { - it.componentType = null - it.componentName = null - it.gpc = PBSUtils.randomNumber as String + and: "Activities set for transmitUfpd with allowing privacy regulation" + def rule = new ActivityRule().tap { + it.privacyRegulation = [IAB_US_GENERAL] } - def activity = Activity.getDefaultActivity([ActivityRule.getDefaultActivityRule(condition, false)]) - def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, activity) - and: "Flush metrics" - flushMetrics(activityPbsService) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true) - and: "Saved account config with allow activities into DB" - def account = getAccountWithAllowActivitiesAndPrivacyModule(accountId, activities) + 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) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest, ["Sec-GPC": VALID_VALUE_FOR_GPC_HEADER]) + def response = 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 + assert metrics[ALERT_GENERAL] == 1 + + and: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $INVALID_GPP_STRING"] + + "Response should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "UsNat privacy module creation failed: Unable to decode UsNatCoreSegment " + + "'${INVALID_GPP_SEGMENT}'. Activity: TRANSMIT_UFPD. Section: ${US_NAT_V1.value}. Gpp: $INVALID_GPP_STRING").size() == 1 } - def "PBS amp call when privacy regulation match and rejecting should remove UFPD fields in request"() { + def "PBS amp call when privacy module contain invalid GPP string shouldn't remove UFPD fields in request and emit warning in response"() { 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" + and: "Default amp request with link to account" + def invalidGpp = PBSUtils.randomString def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value - it.consentString = SIMPLE_GPC_DISALLOW_LOGIC + it.gppSid = US_NAT_V1.value + it.consentString = invalidGpp it.consentType = GPP } and: "Activities set for transmitUfpd with allowing privacy regulation" def rule = new ActivityRule().tap { - it.privacyRegulation = [privacyAllowRegulations] + it.privacyRegulation = [IAB_US_GENERAL] } def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) @@ -1916,42 +2982,56 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) + + then: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) - then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = 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 == 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: - privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + + and: "Should add a warning when in debug mode" + assert response.ext.warnings[PREBID]?.message.contains("GPP string invalid: Unable to decode '$invalidGpp'".toString()) + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $invalidGpp"] } - def "PBS amp call when privacy module contain some part of disallow logic should remove UFPD fields in request"() { + 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value - it.consentString = disallowGppLogic + it.gppSid = gppSid.value + it.consentString = gppConsent it.consentType = GPP } @@ -1977,110 +3057,47 @@ 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( - racialEthnicOrigin: 1, - religiousBeliefs: 1, - healthInfo: 1, - orientation: 1, - citizenshipStatus: 1, - unionMembership: 1, - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataLimitUseNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder() - .setSensitiveDataProcessingOptOutNotice(0) - .setSensitiveDataProcessing(new UsNationalSensitiveData( - racialEthnicOrigin: 2, - religiousBeliefs: 2, - healthInfo: 2, - orientation: 2, - citizenshipStatus: 2, - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - unionMembership: 2, - communicationContents: 2 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 1, - biometricId: 1, - idNumbers: 1, - accountInfo: 1, - communicationContents: 1 - )).build(), - new UspNatV1Consent.Builder().setSensitiveDataProcessing(new UsNationalSensitiveData( - geneticId: 2, - biometricId: 2, - idNumbers: 2, - accountInfo: 2, - communicationContents: 2 - )).build() - ] + gppConsent | gppSid + new UsNatV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_NAT_V1 + new UsCaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CA_V1 + new UsVaV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_VA_V1 + new UsCoV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CO_V1 + new UsUtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_UT_V1 + new UsCtV1Consent.Builder().setMspaServiceProviderMode(MspaMode.YES).setMspaOptOutOptionMode(MspaMode.NO).build() | US_CT_V1 } - def "PBS amp call when request have different gpp consent but match and rejecting should remove UFPD fields in request"() { + 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = gppSid.value - it.consentString = gppConsent + it.gppSid = US_NAT_V1.value + it.consentString = SIMPLE_GPC_DISALLOW_LOGIC it.consentType = GPP } @@ -2091,9 +3108,6 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { 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) @@ -2105,45 +3119,46 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { when: "PBS processes amp request" activityPbsService.sendAmpRequest(ampRequest) - then: "Generic bidder request should have empty UFPD fields" - def genericBidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + then: "Generic bidder request should have data in UFPD fields" + 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 == 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: - 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 + accountGppConfig << [ + new AccountGppConfig(code: IAB_US_GENERAL, enabled: false), + new AccountGppConfig(code: IAB_US_GENERAL, config: new GppModuleConfig(skipSids: [US_NAT_V1]), enabled: true) + ] } - def "PBS amp call when privacy modules contain allowing settings should leave UFPD fields in request"() { + def "PBS amp call when regs.gpp empty in request 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" + and: "Default amp request with link to account" def ampRequest = AmpRequest.defaultAmpRequest.tap { it.account = accountId - it.gppSid = USP_NAT_V1.value - it.consentString = SIMPLE_GPC_DISALLOW_LOGIC + it.gppSid = US_NAT_V1.value + it.consentString = regsGpp it.consentType = GPP } @@ -2154,6 +3169,9 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { 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) @@ -2162,45 +3180,61 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) storedRequestDao.save(storedRequest) + and: "Flush metrics" + flushMetrics(activityPbsService) + when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = 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: "Response shouldn't contain errors" + assert !response.ext.errors + + and: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Metrics processed across activities should be updated" + def metrics = activityPbsService.sendCollectedMetricsRequest() + assert metrics[PROCESSED_ACTIVITY_RULES_COUNT.getValue(ampStoredRequest, TRANSMIT_UFPD)] == 1 + + and: "General alert metric shouldn't be updated" + !metrics[ALERT_GENERAL] 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) - ] + regsGpp << [null, ""] } 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" + and: "Default 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 } @@ -2224,41 +3258,50 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp request" - activityPbsService.sendAmpRequest(ampRequest) + def response = 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: "Response shouldn't contain warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string errors" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $regsGpp"] 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" + and: "Default 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 +3317,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,24 +3331,27 @@ 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() @@ -2315,12 +3361,12 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { 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" + and: "Default 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 +3400,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,41 +3436,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: 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 | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new EqualityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(OR, [new InequalityValueRule(GPC, NO_CONSENT)]) + true | LogicalRestrictedRule.generateSingleRestrictedRule(AND, [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, NO_CONSENT)]) } 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 +3504,51 @@ 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(CONSENT).build() | [new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(false).build() | [new InequalityValueRule(GPC, NO_CONSENT)] + new UsNatV1Consent.Builder().setGpc(true).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(SHARING_NOTICE, CONSENT)] + new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() | [new EqualityValueRule(GPC, NO_CONSENT), + new EqualityValueRule(PERSONAL_DATA_CONSENTS, CONSENT)] } - 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 "PBS amp call when custom privacy regulation empty and normalize is disabled should leave UFPD fields in request and emit error log"() { + given: "Test start time" + def startTime = Instant.now() + + and: "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 +3564,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" @@ -2524,29 +3579,55 @@ class GppTransmitUfpdActivitiesSpec extends PrivacyBaseSpec { storedRequestDao.save(storedRequest) when: "PBS processes amp requests" - activityPbsService.sendAmpRequest(ampRequest) + def response = activityPbsService.sendAmpRequest(ampRequest) - then: "Response should contain error" - def error = thrown(PrebidServerException) - assert error.statusCode == BAD_REQUEST.code() - assert error.responseBody == "Invalid account configuration: JsonLogic exception: " + - "objects must have exactly 1 key defined, found 0" + then: "Response should not contain any warnings" + assert !response.ext.warnings + + and: "Response should contain consent_string error" + assert response.ext.errors[PREBID].message == ["Amp request parameter consent_string has invalid format: $gppConsent"] and: "Metrics for disallowed activities should be updated" def metrics = activityPbsService.sendCollectedMetricsRequest() assert metrics[ALERT_GENERAL] == 1 + + and: "Generic bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + verifyAll { + 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: "Logs should contain error" + def logs = activityPbsService.getLogsByTime(startTime) + assert getLogsByText(logs, "USCustomLogic creation failed: objects must have exactly 1 key defined, found 0").size() == 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 { it.account = accountId it.gppSid = gppSid.intValue - it.consentString = gppStateConsent.build() + it.consentString = gppStateConsent it.consentType = GPP } @@ -2581,100 +3662,152 @@ 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() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(idNumbers: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(accountInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geolocation: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(racialEthnicOrigin: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(communicationContents: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(geneticId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(biometricId: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspCaV1Consent.Builder() - .setSensitiveDataProcessing(new UsCaliforniaSensitiveData(healthInfo: 2)) - USP_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspCaV1Consent.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() - .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() - .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() - .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) - - USP_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), - new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | new UspCoV1Consent.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) - - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(racialEthnicOrigin: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(religiousBeliefs: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(orientation: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(citizenshipStatus: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(healthInfo: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(geneticId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | new UspUtV1Consent.Builder() - .setSensitiveDataProcessing(new UsUtahSensitiveData(biometricId: 2)) - USP_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | new UspUtV1Consent.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() - .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() - .setKnownChildSensitiveDataConsents(PBSUtils.getRandomNumber(0, 2), 1, PBSUtils.getRandomNumber(0, 2)) + gppSid | equalityValueRules | gppStateConsent + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ID_NUMBERS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [idNumbers: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ACCOUNT_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [accountInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geolocation: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_CA_V1, [racialEthnicOrigin: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_COMMUNICATION_CONTENTS, CONSENT)] | generateSensitiveGpp(US_CA_V1, [communicationContents: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [geneticId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_CA_V1, [biometricId: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_CA_V1, [healthInfo: CONSENT]) + US_CA_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_CA_V1, [orientation: CONSENT]) + + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [NO_CONSENT, CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, NO_CONSENT]) + US_CA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CA_V1, [CONSENT, CONSENT]) + + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [NO_CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, NO_CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_VA_V1, [CONSENT, CONSENT]) + US_VA_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_VA_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [NO_CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, NO_CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CO_V1, [CONSENT, CONSENT]) + US_CO_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CO_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RACIAL_ETHNIC_ORIGIN, CONSENT)] | generateSensitiveGpp(US_UT_V1, [racialEthnicOrigin: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_RELIGIOUS_BELIEFS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [religiousBeliefs: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_ORIENTATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [orientation: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_CITIZENSHIP_STATUS, CONSENT)] | generateSensitiveGpp(US_UT_V1, [citizenshipStatus: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_HEALTH_INFO, CONSENT)] | generateSensitiveGpp(US_UT_V1, [healthInfo: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GENETIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geneticId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_BIOMETRIC_ID, CONSENT)] | generateSensitiveGpp(US_UT_V1, [biometricId: CONSENT]) + US_UT_V1 | [new EqualityValueRule(SENSITIVE_DATA_GEOLOCATION, CONSENT)] | generateSensitiveGpp(US_UT_V1, [geolocation: CONSENT]) + + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [NO_CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, NO_CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_UT_V1, [CONSENT, CONSENT]) + US_UT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_UT_V1, [NOT_APPLICABLE, NOT_APPLICABLE]) + + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NOT_APPLICABLE), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NOT_APPLICABLE)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NOT_APPLICABLE, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [NO_CONSENT, CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NOT_APPLICABLE, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, NO_CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, NO_CONSENT, CONSENT]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NOT_APPLICABLE]) + US_CT_V1 | [new EqualityValueRule(CHILD_CONSENTS_BELOW_13, NO_CONSENT), + new EqualityValueRule(CHILD_CONSENTS_FROM_13_TO_16, NO_CONSENT)] | generateChildSensitiveGpp(US_CT_V1, [CONSENT, CONSENT, NO_CONSENT]) } 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,62 +3818,215 @@ 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) { - BidRequest.getDefaultBidRequest().tap { - it.setAccountId(accountId) - it.ext.prebid.trace = VERBOSE - it.device = new Device().tap { - didsha1 = PBSUtils.randomString - didmd5 = PBSUtils.randomString - dpidsha1 = PBSUtils.randomString - ifa = PBSUtils.randomString - macsha1 = PBSUtils.randomString - macmd5 = PBSUtils.randomString - dpidmd5 = PBSUtils.randomString - } - it.user = User.defaultUser - it.user.customdata = PBSUtils.randomString - it.user.eids = [Eid.defaultEid] - it.user.data = [new Data(name: PBSUtils.randomString)] - it.user.buyeruid = PBSUtils.randomString - it.user.yob = PBSUtils.randomNumber - it.user.gender = PBSUtils.randomString - it.user.ext = new UserExt(data: new UserExtData(buyeruid: PBSUtils.randomString)) + def "PBS should remove UFPD fields when privacy regulation match and rejecting and personalDataConsents is 2"() { + 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + 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: "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.geo + !bidderRequest.user.ext + } + + and: "Bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + where: + privacyAllowRegulations << [IAB_US_GENERAL, IAB_ALL, ALL] + } + + def "PBS should remove UFPD fields when privacy regulation match and rejecting and personalDataConsents is 2 and allowPersonalDataConsent2 is false"() { + 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() + } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, config: gppModuleConfig) + + 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: "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.geo + !bidderRequest.user.ext + } + + and: "Bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + where: + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: false) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: null) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: null) + } + + def "PBS shouldn't remove UFPD fields when privacy regulation match and rejecting and personalDataConsents is 2 and allowPersonalDataConsent2 is false"() { + 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 = new UsNatV1Consent.Builder().setPersonalDataConsents(CONSENT).build() } + + and: "Activities set for transmitUfpd with rejecting privacy regulation" + def rule = new ActivityRule(privacyRegulation: [privacyAllowRegulations]) + def activities = AllowActivities.getDefaultAllowActivities(TRANSMIT_UFPD, Activity.getDefaultActivity([rule])) + + and: "Account gpp configuration" + def accountGppConfig = new AccountGppConfig(code: IAB_US_GENERAL, enabled: true, config: gppModuleConfig) + + 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: "Bidder request should have data in UFPD fields" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + verifyAll { + 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: "Bidder request should have data in EIDS fields" + assert bidderRequest.user.eids == bidRequest.user.eids + + where: + privacyAllowRegulations | gppModuleConfig + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2KebabCase: true) + IAB_US_GENERAL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + IAB_ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) + ALL | new GppModuleConfig(allowPersonalDataConsent2SnakeCase: true) } } 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..7d8ed79de7d 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 @@ -12,26 +12,45 @@ import org.prebid.server.functional.model.config.Purpose import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.mock.services.vendorlist.VendorListResponse import org.prebid.server.functional.model.privacy.EnforcementRequirement +import org.prebid.server.functional.model.privacy.gpp.GppDataActivity +import org.prebid.server.functional.model.privacy.gpp.UsCaliforniaV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsCaliforniaV1SensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsColoradoV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsColoradoV1SensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsConnecticutV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsConnecticutV1SensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsUtahV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsUtahV1SensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsVirginiaV1ChildSensitiveData +import org.prebid.server.functional.model.privacy.gpp.UsVirginiaV1SensitiveData +import org.prebid.server.functional.model.request.GppSectionId 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 spock.lang.Shared +import org.prebid.server.functional.util.privacy.gpp.v1.UsCaV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCoV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsCtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsNatV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsUtV1Consent +import org.prebid.server.functional.util.privacy.gpp.v1.UsVaV1Consent import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.bidder.BidderName.OPENX @@ -39,10 +58,18 @@ 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.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_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.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,49 +78,62 @@ 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) - @Shared protected static final int LEG_INT_PURPOSES_ONLY_GVL_VERSION = PBSUtils.getRandomNumberWithExclusion(PURPOSES_ONLY_GVL_VERSION, 0, 4095) - @Shared protected static final int LEG_INT_AND_FLEXIBLE_PURPOSES_GVL_VERSION = PBSUtils.getRandomNumberWithExclusion([PURPOSES_ONLY_GVL_VERSION, LEG_INT_PURPOSES_ONLY_GVL_VERSION], 0, 4095) - @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 INVALID_GPP_SEGMENT = PBSUtils.getRandomString(7) + protected static final String INVALID_GPP_STRING = "DBABLA~${INVALID_GPP_SEGMENT}.YA" 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) + protected static final Map GENERAL_PRIVACY_CONFIG = + GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG - @Shared - protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG) + protected static PrebidServerService privacyPbsService + protected static PrebidServerService activityPbsService def setupSpec() { + privacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GDPR_EEA_COUNTRY) + activityPbsService = pbsServiceFactory.getService(OPENX_CONFIG + SETTING_CONFIG + GENERAL_PRIVACY_CONFIG) vendorListResponse.setResponse() } @@ -103,10 +143,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 +203,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 +244,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 } @@ -493,6 +583,73 @@ abstract class PrivacyBaseSpec extends BaseSpec { enforceVendor: false)] } + protected static String generateSensitiveGpp(GppSectionId sectionId, Map fieldsMap) { + Object sensitiveData + Object consentBuilder + + switch (sectionId) { + case US_CA_V1: + sensitiveData = new UsCaliforniaV1SensitiveData() + consentBuilder = new UsCaV1Consent.Builder() + break + case US_VA_V1: + sensitiveData = new UsVirginiaV1SensitiveData() + consentBuilder = new UsVaV1Consent.Builder() + break + case US_CO_V1: + sensitiveData = new UsColoradoV1SensitiveData() + consentBuilder = new UsCoV1Consent.Builder() + break + case US_UT_V1: + sensitiveData = new UsUtahV1SensitiveData() + consentBuilder = new UsUtV1Consent.Builder() + break + case US_CT_V1: + sensitiveData = new UsConnecticutV1SensitiveData() + consentBuilder = new UsCtV1Consent.Builder() + break + default: + throw new IllegalArgumentException("Unsupported Section ID for Sensitive Data: $sectionId") + } + + fieldsMap.each { fieldName, value -> + sensitiveData.setProperty("$fieldName", value) + } + + consentBuilder.setSensitiveDataProcessing(sensitiveData).build().toString() + } + + protected static String generateChildSensitiveGpp(GppSectionId sectionId, List fields) { + switch (sectionId) { + case US_CA_V1: + return new UsCaV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsCaliforniaV1ChildSensitiveData.getDefault(*fields)) + .build().toString() + + case US_VA_V1: + return new UsVaV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsVirginiaV1ChildSensitiveData.getDefault(fields.first)) + .build().toString() + + case US_CO_V1: + return new UsCoV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsColoradoV1ChildSensitiveData.getDefault(fields.first)) + .build().toString() + + case US_UT_V1: + return new UsUtV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsUtahV1ChildSensitiveData.getDefault(fields.first)) + .build().toString() + + case US_CT_V1: + return new UsCtV1Consent.Builder() + .setKnownChildSensitiveDataConsents(UsConnecticutV1ChildSensitiveData.getDefault(*fields)) + .build().toString() + default: + throw new IllegalArgumentException("Unsupported Section ID for Child Data: $sectionId") + } + } + protected static List getFullTcfCompanyEnforcementRequirementsRandomlyWithExcludePurpose(Purpose purpose) { getFullTcfCompanyEnforcementRequirements(purpose, true) } 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..924ee285661 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,10 +27,14 @@ 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) + private static final PrebidServerService activityPbsServiceExcludeGvl = pbsServiceFactory.getService(PBS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } def "PBS should leave the original request with eids data when requireConsent is enabled and #enforcementRequirements.purpose have any basic consent"() { given: "Default Generic BidRequests with Eid field" @@ -42,7 +46,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 +108,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/TcfFullTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy index 934aa69de33..d798bc4c478 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy @@ -25,7 +25,7 @@ class TcfFullTransmitEidsActivitiesSpec extends PrivacyBaseSpec { private static PrebidServerService privacyPbsServiceWithMultipleGvl def setupSpec() { - privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(PBS_CONFIG) + privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(GENERAL_PRIVACY_CONFIG) def prepareEncodeResponseBodyWithPurposesOnly = getVendorListContent(true, false, false) def prepareEncodeResponseBodyWithLegIntPurposes = getVendorListContent(false, true, false) def prepareEncodeResponseBodyWithLegIntAndFlexiblePurposes = getVendorListContent(false, true, true) 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..e5534476613 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 @@ -54,6 +54,7 @@ class TransmitEidsOrtbConverterActivitiesSpec extends PrivacyBaseSpec { def cleanupSpec() { privacyPbsContainerWithMultipleGvlWithElderOrtb.stop() + pbsServiceFactory.removeContainer(PBS_CONFIG + ["adapters.generic.ortb-version": "2.5"]) } def "PBS should leave the original request with ext.eids data for elder ortb when requireConsent is enabled and #enforcementRequirements.purpose have any basic consent"() { 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..cf5e68bbc90 --- /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]?.width == ampStoredRequest.imp[0].banner.format[0].width + 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