diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8eb8b28c570..00000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2.1 -jobs: - noop: - docker: - - image: cimg/base:stable - steps: - - run: echo "CircleCI build skipped - using GitHub Actions. This job can be removed once 9.x is no longer supported." -workflows: - version: 2 - default: - jobs: - - noop diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 7e14a2d200d..257b4905952 100644 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,5 +1,8 @@ echo "Post Create Starting" +export NVM_DIR="/usr/local/share/nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install nvm use npm install gulp-cli -g diff --git a/.github/actions/install-deb/action.yml b/.github/actions/install-deb/action.yml new file mode 100644 index 00000000000..c33bfb220ba --- /dev/null +++ b/.github/actions/install-deb/action.yml @@ -0,0 +1,35 @@ +name: Install deb +description: Download and install a .deb package +inputs: + url: + description: URL to the .deb file + required: true + name: + description: A local name for the package. Required if using this action multiple times in the same context. + default: package.deb + required: false + +runs: + using: 'composite' + steps: + - name: Restore deb + id: deb-restore + uses: actions/cache/restore@v4 + with: + path: "${{ runner.temp }}/${{ inputs.name }}" + key: ${{ inputs.url }} + - name: Download deb + if: ${{ steps.deb-restore.outputs.cache-hit != 'true' }} + shell: bash + run: | + wget --no-verbose "${{ inputs.url }}" -O "${{ runner.temp }}/${{ inputs.name }}" + - name: Cache deb + if: ${{ steps.deb-restore.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: "${{ runner.temp }}/${{ inputs.name }}" + key: ${{ inputs.url }} + - name: Install deb + shell: bash + run: | + sudo apt-get install -y --allow-downgrades "${{ runner.temp }}/${{ inputs.name }}" diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml new file mode 100644 index 00000000000..0102608dbd1 --- /dev/null +++ b/.github/actions/load/action.yml @@ -0,0 +1,38 @@ +name: Load working directory +description: Load working directory saved with "actions/save" +inputs: + name: + description: The name used with actions/save + +runs: + using: 'composite' + steps: + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + - uses: actions/github-script@v8 + id: platform + with: + result-encoding: string + script: | + const os = require('os'); + return os.platform(); + - name: 'Clear working directory' + shell: bash + run: | + rm -r "$(pwd)"/* + + - name: Download artifact + uses: actions/download-artifact@v5 + with: + path: '${{ runner.temp }}' + name: '${{ inputs.name }}' + + - name: 'Untar working directory' + shell: bash + run: | + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ steps.platform.outputs.result == 'win32' && '--force-local' || '' }} -C "$parent" -xf '${{ runner.temp }}/${{ inputs.name }}.tar' "$target" diff --git a/.github/actions/npm-ci/action.yml b/.github/actions/npm-ci/action.yml new file mode 100644 index 00000000000..c23b3f455d6 --- /dev/null +++ b/.github/actions/npm-ci/action.yml @@ -0,0 +1,23 @@ +name: NPM install +description: Run npm install and cache dependencies + +runs: + using: 'composite' + steps: + - name: Restore dependencies + id: restore-modules + uses: actions/cache/restore@v4 + with: + path: "node_modules" + key: node_modules-${{ hashFiles('package-lock.json') }} + - name: Run npm ci + if: ${{ steps.restore-modules.outputs.cache-hit != 'true' }} + shell: bash + run: | + npm ci + - name: Cache dependencies + if: ${{ steps.restore-modules.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: "node_modules" + key: node_modules-${{ hashFiles('package-lock.json') }} diff --git a/.github/actions/save/action.yml b/.github/actions/save/action.yml new file mode 100644 index 00000000000..3efca584c7f --- /dev/null +++ b/.github/actions/save/action.yml @@ -0,0 +1,41 @@ +name: Save working directory +description: Save working directory, preserving permissions +inputs: + prefix: + description: Prefix to use for autogenerated names + required: false + name: + description: a name to reference with actions/load + required: false +outputs: + name: + description: a name to reference with actions/load + value: ${{ fromJSON(steps.platform.outputs.result).name }} + +runs: + using: 'composite' + steps: + - uses: actions/github-script@v8 + id: platform + with: + script: | + const os = require('os'); + const crypto = require("crypto"); + const id = crypto.randomBytes(16).toString("hex"); + return { + name: ${{ inputs.name && format('"{0}"', inputs.name) || format('"{0}" + id', inputs.prefix || '') }}, + platform: os.platform(), + } + - name: Tar working directory + shell: bash + run: | + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ fromJSON(steps.platform.outputs.result).platform == 'win32' && '--force-local' || '' }} -C "$parent" -cf "${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar" "$target" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + path: '${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar' + name: ${{ fromJSON(steps.platform.outputs.result).name }} + overwrite: true diff --git a/.github/actions/wait-for-browserstack/action.yml b/.github/actions/wait-for-browserstack/action.yml new file mode 100644 index 00000000000..3242e29fc50 --- /dev/null +++ b/.github/actions/wait-for-browserstack/action.yml @@ -0,0 +1,27 @@ +name: Wait for browserstack sessions +description: Wait until enough browserstack sessions have become available +inputs: + sessions: + description: Number of sessions needed to continue + default: "6" +runs: + using: 'composite' + steps: + - shell: bash + run: | + while + status=$(curl -u "${BROWSERSTACK_USERNAME}:${BROWSERSTACK_ACCESS_KEY}" \ + -X GET "https://api-cloud.browserstack.com/automate/plan.json" 2> /dev/null); + running=$(jq '.parallel_sessions_running' <<< $status) + max_running=$(jq '.parallel_sessions_max_allowed' <<< $status) + queued=$(jq '.queued_sessions' <<< $status) + max_queued=$(jq '.queued_sessions_max_allowed' <<< $status) + spare=$(( ${max_running} + ${max_queued} - ${running} - ${queued} )) + required=${{ inputs.sessions }} + echo "Browserstack status: ${running} sessions running, ${queued} queued, ${spare} free" + (( ${required} > ${spare} )) + do + delay=$(( 60 + $(shuf -i 1-60 -n 1) )) + echo "Waiting for ${required} sessions to free up, checking again in ${delay}s" + sleep $delay + done diff --git a/.github/codeql/queries/autogen_2d_RenderingContext_getImageData.ql b/.github/codeql/queries/autogen_2d_RenderingContext_getImageData.ql deleted file mode 100644 index 9323058df37..00000000000 --- a/.github/codeql/queries/autogen_2d_RenderingContext_getImageData.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/2d-getimagedata - * @name Access to 2d rendering context getImageData - * @kind problem - * @problem.severity warning - * @description Finds uses of 2d RenderingContext.getImageData - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("2d") and - api = invocation.getAPropertyRead("getImageData") -select api, "getImageData is an indicator of fingerprinting, weighed 38.11 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_2d_RenderingContext_isPointInPath.ql b/.github/codeql/queries/autogen_2d_RenderingContext_isPointInPath.ql deleted file mode 100644 index 2318a0c8400..00000000000 --- a/.github/codeql/queries/autogen_2d_RenderingContext_isPointInPath.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/2d-ispointinpath - * @name Access to 2d rendering context isPointInPath - * @kind problem - * @problem.severity warning - * @description Finds uses of 2d RenderingContext.isPointInPath - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("2d") and - api = invocation.getAPropertyRead("isPointInPath") -select api, "isPointInPath is an indicator of fingerprinting, weighed 4216.23 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_2d_RenderingContext_measureText.ql b/.github/codeql/queries/autogen_2d_RenderingContext_measureText.ql deleted file mode 100644 index b862cd21b51..00000000000 --- a/.github/codeql/queries/autogen_2d_RenderingContext_measureText.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/2d-measuretext - * @name Access to 2d rendering context measureText - * @kind problem - * @problem.severity warning - * @description Finds uses of 2d RenderingContext.measureText - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("2d") and - api = invocation.getAPropertyRead("measureText") -select api, "measureText is an indicator of fingerprinting, weighed 48.2 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_AudioWorkletNode.ql b/.github/codeql/queries/autogen_AudioWorkletNode.ql deleted file mode 100644 index de9f13c2104..00000000000 --- a/.github/codeql/queries/autogen_AudioWorkletNode.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/audioworkletnode - * @name Use of AudioWorkletNode - * @kind problem - * @problem.severity warning - * @description Finds uses of AudioWorkletNode - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = instantiationOf("AudioWorkletNode") -select api, "AudioWorkletNode is an indicator of fingerprinting, weighed 32.68 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Date_getTimezoneOffset.ql b/.github/codeql/queries/autogen_Date_getTimezoneOffset.ql deleted file mode 100644 index 43c4a8bcefa..00000000000 --- a/.github/codeql/queries/autogen_Date_getTimezoneOffset.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/date-gettimezoneoffset - * @name Access to Date.getTimezoneOffset - * @kind problem - * @problem.severity warning - * @description Finds uses of Date.getTimezoneOffset - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode inst, SourceNode api -where - inst = instantiationOf("Date") and - api = inst.getAPropertyRead("getTimezoneOffset") -select api, "getTimezoneOffset is an indicator of fingerprinting, weighed 16.79 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_DeviceMotionEvent_acceleration.ql b/.github/codeql/queries/autogen_DeviceMotionEvent_acceleration.ql deleted file mode 100644 index 21a268b3f68..00000000000 --- a/.github/codeql/queries/autogen_DeviceMotionEvent_acceleration.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/devicemotionevent-acceleration - * @name Potential access to DeviceMotionEvent.acceleration - * @kind problem - * @problem.severity warning - * @description Finds uses of acceleration - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from PropRef api -where - api.getPropertyName() = "acceleration" -select api, "acceleration is an indicator of fingerprinting, weighed 59.51 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_DeviceMotionEvent_accelerationIncludingGravity.ql b/.github/codeql/queries/autogen_DeviceMotionEvent_accelerationIncludingGravity.ql deleted file mode 100644 index 1c970bca6ed..00000000000 --- a/.github/codeql/queries/autogen_DeviceMotionEvent_accelerationIncludingGravity.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/devicemotionevent-accelerationincludinggravity - * @name Potential access to DeviceMotionEvent.accelerationIncludingGravity - * @kind problem - * @problem.severity warning - * @description Finds uses of accelerationIncludingGravity - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from PropRef api -where - api.getPropertyName() = "accelerationIncludingGravity" -select api, "accelerationIncludingGravity is an indicator of fingerprinting, weighed 166.43 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_DeviceMotionEvent_rotationRate.ql b/.github/codeql/queries/autogen_DeviceMotionEvent_rotationRate.ql deleted file mode 100644 index 5256f3295fb..00000000000 --- a/.github/codeql/queries/autogen_DeviceMotionEvent_rotationRate.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/devicemotionevent-rotationrate - * @name Potential access to DeviceMotionEvent.rotationRate - * @kind problem - * @problem.severity warning - * @description Finds uses of rotationRate - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from PropRef api -where - api.getPropertyName() = "rotationRate" -select api, "rotationRate is an indicator of fingerprinting, weighed 59.32 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Gyroscope.ql b/.github/codeql/queries/autogen_Gyroscope.ql deleted file mode 100644 index d57c464e0eb..00000000000 --- a/.github/codeql/queries/autogen_Gyroscope.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/gyroscope - * @name Use of Gyroscope - * @kind problem - * @problem.severity warning - * @description Finds uses of Gyroscope - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = instantiationOf("Gyroscope") -select api, "Gyroscope is an indicator of fingerprinting, weighed 189.7 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Gyroscope_x.ql b/.github/codeql/queries/autogen_Gyroscope_x.ql deleted file mode 100644 index 3344b446ddb..00000000000 --- a/.github/codeql/queries/autogen_Gyroscope_x.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/gyroscope-x - * @name Access to Gyroscope.x - * @kind problem - * @problem.severity warning - * @description Finds uses of Gyroscope.x - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode inst, SourceNode api -where - inst = instantiationOf("Gyroscope") and - api = inst.getAPropertyRead("x") -select api, "x is an indicator of fingerprinting, weighed 4216.23 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Gyroscope_y.ql b/.github/codeql/queries/autogen_Gyroscope_y.ql deleted file mode 100644 index b5b75a1c5cd..00000000000 --- a/.github/codeql/queries/autogen_Gyroscope_y.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/gyroscope-y - * @name Access to Gyroscope.y - * @kind problem - * @problem.severity warning - * @description Finds uses of Gyroscope.y - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode inst, SourceNode api -where - inst = instantiationOf("Gyroscope") and - api = inst.getAPropertyRead("y") -select api, "y is an indicator of fingerprinting, weighed 4216.23 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Gyroscope_z.ql b/.github/codeql/queries/autogen_Gyroscope_z.ql deleted file mode 100644 index 417556ad8ee..00000000000 --- a/.github/codeql/queries/autogen_Gyroscope_z.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/gyroscope-z - * @name Access to Gyroscope.z - * @kind problem - * @problem.severity warning - * @description Finds uses of Gyroscope.z - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode inst, SourceNode api -where - inst = instantiationOf("Gyroscope") and - api = inst.getAPropertyRead("z") -select api, "z is an indicator of fingerprinting, weighed 4216.23 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_Notification_permission.ql b/.github/codeql/queries/autogen_Notification_permission.ql deleted file mode 100644 index 555cb9b81af..00000000000 --- a/.github/codeql/queries/autogen_Notification_permission.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/notification-permission - * @name Access to Notification.permission - * @kind problem - * @problem.severity warning - * @description Finds uses of Notification.permission - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("Notification") and - api = prop.getAPropertyRead("permission") -select api, "permission is an indicator of fingerprinting, weighed 21.19 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_OfflineAudioContext.ql b/.github/codeql/queries/autogen_OfflineAudioContext.ql deleted file mode 100644 index c3406125a07..00000000000 --- a/.github/codeql/queries/autogen_OfflineAudioContext.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/offlineaudiocontext - * @name Use of OfflineAudioContext - * @kind problem - * @problem.severity warning - * @description Finds uses of OfflineAudioContext - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = instantiationOf("OfflineAudioContext") -select api, "OfflineAudioContext is an indicator of fingerprinting, weighed 1062.57 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_RTCPeerConnection.ql b/.github/codeql/queries/autogen_RTCPeerConnection.ql deleted file mode 100644 index 8d7e80174ee..00000000000 --- a/.github/codeql/queries/autogen_RTCPeerConnection.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/rtcpeerconnection - * @name Use of RTCPeerConnection - * @kind problem - * @problem.severity warning - * @description Finds uses of RTCPeerConnection - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = instantiationOf("RTCPeerConnection") -select api, "RTCPeerConnection is an indicator of fingerprinting, weighed 47.04 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_SharedWorker.ql b/.github/codeql/queries/autogen_SharedWorker.ql deleted file mode 100644 index 1b2859b8523..00000000000 --- a/.github/codeql/queries/autogen_SharedWorker.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/sharedworker - * @name Use of SharedWorker - * @kind problem - * @problem.severity warning - * @description Finds uses of SharedWorker - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = instantiationOf("SharedWorker") -select api, "SharedWorker is an indicator of fingerprinting, weighed 56.71 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_fpDOMMethod.qll b/.github/codeql/queries/autogen_fpDOMMethod.qll new file mode 100644 index 00000000000..7e21d791a69 --- /dev/null +++ b/.github/codeql/queries/autogen_fpDOMMethod.qll @@ -0,0 +1,23 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class DOMMethod extends string { + + float weight; + string type; + + DOMMethod() { + + ( this = "toDataURL" and weight = 25.89 and type = "HTMLCanvasElement" ) + or + ( this = "getChannelData" and weight = 806.52 and type = "AudioBuffer" ) + } + + float getWeight() { + result = weight + } + + string getType() { + result = type + } + +} diff --git a/.github/codeql/queries/autogen_fpEventProperty.qll b/.github/codeql/queries/autogen_fpEventProperty.qll new file mode 100644 index 00000000000..d136c7a6ab6 --- /dev/null +++ b/.github/codeql/queries/autogen_fpEventProperty.qll @@ -0,0 +1,35 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class EventProperty extends string { + + float weight; + string event; + + EventProperty() { + + ( this = "accelerationIncludingGravity" and weight = 158.1 and event = "devicemotion" ) + or + ( this = "beta" and weight = 887.22 and event = "deviceorientation" ) + or + ( this = "gamma" and weight = 361.7 and event = "deviceorientation" ) + or + ( this = "alpha" and weight = 354.09 and event = "deviceorientation" ) + or + ( this = "candidate" and weight = 69.81 and event = "icecandidate" ) + or + ( this = "acceleration" and weight = 64.92 and event = "devicemotion" ) + or + ( this = "rotationRate" and weight = 64.37 and event = "devicemotion" ) + or + ( this = "absolute" and weight = 709.73 and event = "deviceorientation" ) + } + + float getWeight() { + result = weight + } + + string getEvent() { + result = event + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalConstructor.qll b/.github/codeql/queries/autogen_fpGlobalConstructor.qll new file mode 100644 index 00000000000..43213748fa3 --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalConstructor.qll @@ -0,0 +1,24 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalConstructor extends string { + + float weight; + + GlobalConstructor() { + + ( this = "OfflineAudioContext" and weight = 1111.66 ) + or + ( this = "SharedWorker" and weight = 93.35 ) + or + ( this = "RTCPeerConnection" and weight = 49.52 ) + or + ( this = "Gyroscope" and weight = 98.72 ) + or + ( this = "AudioWorkletNode" and weight = 72.93 ) + } + + float getWeight() { + result = weight + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll new file mode 100644 index 00000000000..bd815ca8cce --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalObjectProperty0.qll @@ -0,0 +1,71 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalObjectProperty0 extends string { + + float weight; + string global0; + + GlobalObjectProperty0() { + + ( this = "cookieEnabled" and weight = 15.36 and global0 = "navigator" ) + or + ( this = "availHeight" and weight = 69.48 and global0 = "screen" ) + or + ( this = "availWidth" and weight = 65.15 and global0 = "screen" ) + or + ( this = "colorDepth" and weight = 34.39 and global0 = "screen" ) + or + ( this = "deviceMemory" and weight = 75.15 and global0 = "navigator" ) + or + ( this = "availTop" and weight = 1256.76 and global0 = "screen" ) + or + ( this = "getBattery" and weight = 124.12 and global0 = "navigator" ) + or + ( this = "webdriver" and weight = 30.18 and global0 = "navigator" ) + or + ( this = "permission" and weight = 22.23 and global0 = "Notification" ) + or + ( this = "storage" and weight = 170.65 and global0 = "navigator" ) + or + ( this = "orientation" and weight = 38.3 and global0 = "screen" ) + or + ( this = "onLine" and weight = 20.05 and global0 = "navigator" ) + or + ( this = "pixelDepth" and weight = 38.22 and global0 = "screen" ) + or + ( this = "availLeft" and weight = 539.55 and global0 = "screen" ) + or + ( this = "vendorSub" and weight = 1462.45 and global0 = "navigator" ) + or + ( this = "productSub" and weight = 525.88 and global0 = "navigator" ) + or + ( this = "webkitTemporaryStorage" and weight = 40.85 and global0 = "navigator" ) + or + ( this = "hardwareConcurrency" and weight = 70.43 and global0 = "navigator" ) + or + ( this = "appCodeName" and weight = 152.93 and global0 = "navigator" ) + or + ( this = "keyboard" and weight = 2426.5 and global0 = "navigator" ) + or + ( this = "mediaDevices" and weight = 123.07 and global0 = "navigator" ) + or + ( this = "mediaCapabilities" and weight = 124.39 and global0 = "navigator" ) + or + ( this = "permissions" and weight = 70.22 and global0 = "navigator" ) + or + ( this = "webkitPersistentStorage" and weight = 113.71 and global0 = "navigator" ) + or + ( this = "requestMediaKeySystemAccess" and weight = 16.88 and global0 = "navigator" ) + or + ( this = "getGamepads" and weight = 202.54 and global0 = "navigator" ) + } + + float getWeight() { + result = weight + } + + string getGlobal0() { + result = global0 + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll b/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll new file mode 100644 index 00000000000..b874d860835 --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalObjectProperty1.qll @@ -0,0 +1,26 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalObjectProperty1 extends string { + + float weight; + string global0; + string global1; + + GlobalObjectProperty1() { + + ( this = "enumerateDevices" and weight = 329.65 and global0 = "navigator" and global1 = "mediaDevices" ) + } + + float getWeight() { + result = weight + } + + string getGlobal0() { + result = global0 + } + + string getGlobal1() { + result = global1 + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll b/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll new file mode 100644 index 00000000000..df96f92eaed --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalTypeProperty0.qll @@ -0,0 +1,25 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalTypeProperty0 extends string { + + float weight; + string global0; + + GlobalTypeProperty0() { + + ( this = "x" and weight = 7033.93 and global0 = "Gyroscope" ) + or + ( this = "y" and weight = 7033.93 and global0 = "Gyroscope" ) + or + ( this = "z" and weight = 7033.93 and global0 = "Gyroscope" ) + } + + float getWeight() { + result = weight + } + + string getGlobal0() { + result = global0 + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll b/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll new file mode 100644 index 00000000000..dbdf06d0d47 --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalTypeProperty1.qll @@ -0,0 +1,26 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalTypeProperty1 extends string { + + float weight; + string global0; + string global1; + + GlobalTypeProperty1() { + + ( this = "resolvedOptions" and weight = 18.83 and global0 = "Intl" and global1 = "DateTimeFormat" ) + } + + float getWeight() { + result = weight + } + + string getGlobal0() { + result = global0 + } + + string getGlobal1() { + result = global1 + } + +} diff --git a/.github/codeql/queries/autogen_fpGlobalVar.qll b/.github/codeql/queries/autogen_fpGlobalVar.qll new file mode 100644 index 00000000000..7a337b3519d --- /dev/null +++ b/.github/codeql/queries/autogen_fpGlobalVar.qll @@ -0,0 +1,32 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class GlobalVar extends string { + + float weight; + + GlobalVar() { + + ( this = "devicePixelRatio" and weight = 18.91 ) + or + ( this = "screenX" and weight = 355.18 ) + or + ( this = "screenY" and weight = 309.2 ) + or + ( this = "outerWidth" and weight = 109.86 ) + or + ( this = "outerHeight" and weight = 178.05 ) + or + ( this = "screenLeft" and weight = 374.27 ) + or + ( this = "screenTop" and weight = 373.73 ) + or + ( this = "indexedDB" and weight = 18.81 ) + or + ( this = "openDatabase" and weight = 134.7 ) + } + + float getWeight() { + result = weight + } + +} diff --git a/.github/codeql/queries/autogen_fpRenderingContextProperty.qll b/.github/codeql/queries/autogen_fpRenderingContextProperty.qll new file mode 100644 index 00000000000..510e393b984 --- /dev/null +++ b/.github/codeql/queries/autogen_fpRenderingContextProperty.qll @@ -0,0 +1,49 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class RenderingContextProperty extends string { + + float weight; + string contextType; + + RenderingContextProperty() { + + ( this = "getExtension" and weight = 17.76 and contextType = "webgl" ) + or + ( this = "getParameter" and weight = 20.31 and contextType = "webgl" ) + or + ( this = "getParameter" and weight = 65.17 and contextType = "webgl2" ) + or + ( this = "getShaderPrecisionFormat" and weight = 107.03 and contextType = "webgl2" ) + or + ( this = "getExtension" and weight = 70.03 and contextType = "webgl2" ) + or + ( this = "getContextAttributes" and weight = 175.38 and contextType = "webgl2" ) + or + ( this = "getSupportedExtensions" and weight = 487.31 and contextType = "webgl2" ) + or + ( this = "getImageData" and weight = 44.3 and contextType = "2d" ) + or + ( this = "measureText" and weight = 47.23 and contextType = "2d" ) + or + ( this = "getShaderPrecisionFormat" and weight = 595.72 and contextType = "webgl" ) + or + ( this = "getContextAttributes" and weight = 1038.26 and contextType = "webgl" ) + or + ( this = "getSupportedExtensions" and weight = 805.83 and contextType = "webgl" ) + or + ( this = "readPixels" and weight = 20.6 and contextType = "webgl" ) + or + ( this = "isPointInPath" and weight = 7033.93 and contextType = "2d" ) + or + ( this = "readPixels" and weight = 73.62 and contextType = "webgl2" ) + } + + float getWeight() { + result = weight + } + + string getContextType() { + result = contextType + } + +} diff --git a/.github/codeql/queries/autogen_fpSensorProperty.qll b/.github/codeql/queries/autogen_fpSensorProperty.qll new file mode 100644 index 00000000000..776a78d8434 --- /dev/null +++ b/.github/codeql/queries/autogen_fpSensorProperty.qll @@ -0,0 +1,16 @@ +// this file is autogenerated, see fingerprintApis.mjs + +class SensorProperty extends string { + + float weight; + + SensorProperty() { + + ( this = "start" and weight = 104.06 ) + } + + float getWeight() { + result = weight + } + +} diff --git a/.github/codeql/queries/autogen_navigator_appCodeName.ql b/.github/codeql/queries/autogen_navigator_appCodeName.ql deleted file mode 100644 index f6a5cd10bf3..00000000000 --- a/.github/codeql/queries/autogen_navigator_appCodeName.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-appcodename - * @name Access to navigator.appCodeName - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.appCodeName - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("appCodeName") -select api, "appCodeName is an indicator of fingerprinting, weighed 127.81 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_deviceMemory.ql b/.github/codeql/queries/autogen_navigator_deviceMemory.ql deleted file mode 100644 index 476a2927f12..00000000000 --- a/.github/codeql/queries/autogen_navigator_deviceMemory.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-devicememory - * @name Access to navigator.deviceMemory - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.deviceMemory - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("deviceMemory") -select api, "deviceMemory is an indicator of fingerprinting, weighed 78.47 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_getBattery.ql b/.github/codeql/queries/autogen_navigator_getBattery.ql deleted file mode 100644 index a039ebd2908..00000000000 --- a/.github/codeql/queries/autogen_navigator_getBattery.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-getbattery - * @name Access to navigator.getBattery - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.getBattery - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("getBattery") -select api, "getBattery is an indicator of fingerprinting, weighed 117.75 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_getGamepads.ql b/.github/codeql/queries/autogen_navigator_getGamepads.ql deleted file mode 100644 index 491b398a4fb..00000000000 --- a/.github/codeql/queries/autogen_navigator_getGamepads.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-getgamepads - * @name Access to navigator.getGamepads - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.getGamepads - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("getGamepads") -select api, "getGamepads is an indicator of fingerprinting, weighed 256.1 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_hardwareConcurrency.ql b/.github/codeql/queries/autogen_navigator_hardwareConcurrency.ql deleted file mode 100644 index dfb685511a2..00000000000 --- a/.github/codeql/queries/autogen_navigator_hardwareConcurrency.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-hardwareconcurrency - * @name Access to navigator.hardwareConcurrency - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.hardwareConcurrency - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("hardwareConcurrency") -select api, "hardwareConcurrency is an indicator of fingerprinting, weighed 62.49 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_keyboard.ql b/.github/codeql/queries/autogen_navigator_keyboard.ql deleted file mode 100644 index 9a941c755f8..00000000000 --- a/.github/codeql/queries/autogen_navigator_keyboard.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-keyboard - * @name Access to navigator.keyboard - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.keyboard - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("keyboard") -select api, "keyboard is an indicator of fingerprinting, weighed 1296.7 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_mediaCapabilities.ql b/.github/codeql/queries/autogen_navigator_mediaCapabilities.ql deleted file mode 100644 index 7547d6aa8d2..00000000000 --- a/.github/codeql/queries/autogen_navigator_mediaCapabilities.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-mediacapabilities - * @name Access to navigator.mediaCapabilities - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.mediaCapabilities - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("mediaCapabilities") -select api, "mediaCapabilities is an indicator of fingerprinting, weighed 115.38 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_mediaDevices.ql b/.github/codeql/queries/autogen_navigator_mediaDevices.ql deleted file mode 100644 index de8b5c470dc..00000000000 --- a/.github/codeql/queries/autogen_navigator_mediaDevices.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-mediadevices - * @name Access to navigator.mediaDevices - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.mediaDevices - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("mediaDevices") -select api, "mediaDevices is an indicator of fingerprinting, weighed 110.44 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_onLine.ql b/.github/codeql/queries/autogen_navigator_onLine.ql deleted file mode 100644 index 18061b2986d..00000000000 --- a/.github/codeql/queries/autogen_navigator_onLine.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-online - * @name Access to navigator.onLine - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.onLine - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("onLine") -select api, "onLine is an indicator of fingerprinting, weighed 19.11 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_permissions.ql b/.github/codeql/queries/autogen_navigator_permissions.ql deleted file mode 100644 index dcc0c2930c2..00000000000 --- a/.github/codeql/queries/autogen_navigator_permissions.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-permissions - * @name Access to navigator.permissions - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.permissions - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("permissions") -select api, "permissions is an indicator of fingerprinting, weighed 72.06 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_productSub.ql b/.github/codeql/queries/autogen_navigator_productSub.ql deleted file mode 100644 index ca28abef4cf..00000000000 --- a/.github/codeql/queries/autogen_navigator_productSub.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-productsub - * @name Access to navigator.productSub - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.productSub - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("productSub") -select api, "productSub is an indicator of fingerprinting, weighed 475.08 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_requestMediaKeySystemAccess.ql b/.github/codeql/queries/autogen_navigator_requestMediaKeySystemAccess.ql deleted file mode 100644 index 3d99aae1178..00000000000 --- a/.github/codeql/queries/autogen_navigator_requestMediaKeySystemAccess.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-requestmediakeysystemaccess - * @name Access to navigator.requestMediaKeySystemAccess - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.requestMediaKeySystemAccess - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("requestMediaKeySystemAccess") -select api, "requestMediaKeySystemAccess is an indicator of fingerprinting, weighed 15.3 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_storage.ql b/.github/codeql/queries/autogen_navigator_storage.ql deleted file mode 100644 index 5c96fc5350a..00000000000 --- a/.github/codeql/queries/autogen_navigator_storage.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-storage - * @name Access to navigator.storage - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.storage - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("storage") -select api, "storage is an indicator of fingerprinting, weighed 143.61 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_vendorSub.ql b/.github/codeql/queries/autogen_navigator_vendorSub.ql deleted file mode 100644 index cca8739f8df..00000000000 --- a/.github/codeql/queries/autogen_navigator_vendorSub.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-vendorsub - * @name Access to navigator.vendorSub - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.vendorSub - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("vendorSub") -select api, "vendorSub is an indicator of fingerprinting, weighed 1543.86 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_webdriver.ql b/.github/codeql/queries/autogen_navigator_webdriver.ql deleted file mode 100644 index c6e5f5affca..00000000000 --- a/.github/codeql/queries/autogen_navigator_webdriver.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-webdriver - * @name Access to navigator.webdriver - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.webdriver - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("webdriver") -select api, "webdriver is an indicator of fingerprinting, weighed 32.18 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_webkitPersistentStorage.ql b/.github/codeql/queries/autogen_navigator_webkitPersistentStorage.ql deleted file mode 100644 index 646c73b83bb..00000000000 --- a/.github/codeql/queries/autogen_navigator_webkitPersistentStorage.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-webkitpersistentstorage - * @name Access to navigator.webkitPersistentStorage - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.webkitPersistentStorage - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("webkitPersistentStorage") -select api, "webkitPersistentStorage is an indicator of fingerprinting, weighed 132.06 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_navigator_webkitTemporaryStorage.ql b/.github/codeql/queries/autogen_navigator_webkitTemporaryStorage.ql deleted file mode 100644 index 1dee42c327f..00000000000 --- a/.github/codeql/queries/autogen_navigator_webkitTemporaryStorage.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/navigator-webkittemporarystorage - * @name Access to navigator.webkitTemporaryStorage - * @kind problem - * @problem.severity warning - * @description Finds uses of navigator.webkitTemporaryStorage - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("navigator") and - api = prop.getAPropertyRead("webkitTemporaryStorage") -select api, "webkitTemporaryStorage is an indicator of fingerprinting, weighed 42.05 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_availHeight.ql b/.github/codeql/queries/autogen_screen_availHeight.ql deleted file mode 100644 index e0ca9acf366..00000000000 --- a/.github/codeql/queries/autogen_screen_availHeight.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-availheight - * @name Access to screen.availHeight - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.availHeight - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("availHeight") -select api, "availHeight is an indicator of fingerprinting, weighed 72.8 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_availLeft.ql b/.github/codeql/queries/autogen_screen_availLeft.ql deleted file mode 100644 index f8cafbee083..00000000000 --- a/.github/codeql/queries/autogen_screen_availLeft.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-availleft - * @name Access to screen.availLeft - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.availLeft - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("availLeft") -select api, "availLeft is an indicator of fingerprinting, weighed 665.2 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_availTop.ql b/.github/codeql/queries/autogen_screen_availTop.ql deleted file mode 100644 index 8fb1cfa15b0..00000000000 --- a/.github/codeql/queries/autogen_screen_availTop.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-availtop - * @name Access to screen.availTop - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.availTop - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("availTop") -select api, "availTop is an indicator of fingerprinting, weighed 1527.75 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_availWidth.ql b/.github/codeql/queries/autogen_screen_availWidth.ql deleted file mode 100644 index 33445cafaf3..00000000000 --- a/.github/codeql/queries/autogen_screen_availWidth.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-availwidth - * @name Access to screen.availWidth - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.availWidth - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("availWidth") -select api, "availWidth is an indicator of fingerprinting, weighed 66.83 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_colorDepth.ql b/.github/codeql/queries/autogen_screen_colorDepth.ql deleted file mode 100644 index d60e95e1fbb..00000000000 --- a/.github/codeql/queries/autogen_screen_colorDepth.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-colordepth - * @name Access to screen.colorDepth - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.colorDepth - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("colorDepth") -select api, "colorDepth is an indicator of fingerprinting, weighed 33.93 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_orientation.ql b/.github/codeql/queries/autogen_screen_orientation.ql deleted file mode 100644 index 4dff29ca88c..00000000000 --- a/.github/codeql/queries/autogen_screen_orientation.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-orientation - * @name Access to screen.orientation - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.orientation - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("orientation") -select api, "orientation is an indicator of fingerprinting, weighed 43.44 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_screen_pixelDepth.ql b/.github/codeql/queries/autogen_screen_pixelDepth.ql deleted file mode 100644 index 8848e614961..00000000000 --- a/.github/codeql/queries/autogen_screen_pixelDepth.ql +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @id prebid/screen-pixeldepth - * @name Access to screen.pixelDepth - * @kind problem - * @problem.severity warning - * @description Finds uses of screen.pixelDepth - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("screen") and - api = prop.getAPropertyRead("pixelDepth") -select api, "pixelDepth is an indicator of fingerprinting, weighed 34.57 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_getContextAttributes.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_getContextAttributes.ql deleted file mode 100644 index 907d2af6b63..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_getContextAttributes.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-getcontextattributes - * @name Access to webgl2 rendering context getContextAttributes - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.getContextAttributes - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("getContextAttributes") -select api, "getContextAttributes is an indicator of fingerprinting, weighed 175.29 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_getExtension.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_getExtension.ql deleted file mode 100644 index 97deebacbf8..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_getExtension.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-getextension - * @name Access to webgl2 rendering context getExtension - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.getExtension - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("getExtension") -select api, "getExtension is an indicator of fingerprinting, weighed 44.76 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_getParameter.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_getParameter.ql deleted file mode 100644 index 77a25606be0..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_getParameter.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-getparameter - * @name Access to webgl2 rendering context getParameter - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.getParameter - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("getParameter") -select api, "getParameter is an indicator of fingerprinting, weighed 42.17 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_getShaderPrecisionFormat.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_getShaderPrecisionFormat.ql deleted file mode 100644 index f2b0de4d528..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_getShaderPrecisionFormat.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-getshaderprecisionformat - * @name Access to webgl2 rendering context getShaderPrecisionFormat - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.getShaderPrecisionFormat - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("getShaderPrecisionFormat") -select api, "getShaderPrecisionFormat is an indicator of fingerprinting, weighed 105.96 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_getSupportedExtensions.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_getSupportedExtensions.ql deleted file mode 100644 index 5039c59294a..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_getSupportedExtensions.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-getsupportedextensions - * @name Access to webgl2 rendering context getSupportedExtensions - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.getSupportedExtensions - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("getSupportedExtensions") -select api, "getSupportedExtensions is an indicator of fingerprinting, weighed 495.53 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl2_RenderingContext_readPixels.ql b/.github/codeql/queries/autogen_webgl2_RenderingContext_readPixels.ql deleted file mode 100644 index 1b89fb6b857..00000000000 --- a/.github/codeql/queries/autogen_webgl2_RenderingContext_readPixels.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl2-readpixels - * @name Access to webgl2 rendering context readPixels - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl2 RenderingContext.readPixels - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl2") and - api = invocation.getAPropertyRead("readPixels") -select api, "readPixels is an indicator of fingerprinting, weighed 61.17 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_getContextAttributes.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_getContextAttributes.ql deleted file mode 100644 index 1ad0af71669..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_getContextAttributes.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-getcontextattributes - * @name Access to webgl rendering context getContextAttributes - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.getContextAttributes - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("getContextAttributes") -select api, "getContextAttributes is an indicator of fingerprinting, weighed 2131.59 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_getExtension.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_getExtension.ql deleted file mode 100644 index 87922396ad0..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_getExtension.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-getextension - * @name Access to webgl rendering context getExtension - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.getExtension - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("getExtension") -select api, "getExtension is an indicator of fingerprinting, weighed 19.17 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_getParameter.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_getParameter.ql deleted file mode 100644 index 2a5e408e668..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_getParameter.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-getparameter - * @name Access to webgl rendering context getParameter - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.getParameter - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("getParameter") -select api, "getParameter is an indicator of fingerprinting, weighed 21.25 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_getShaderPrecisionFormat.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_getShaderPrecisionFormat.ql deleted file mode 100644 index ac6972b348e..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_getShaderPrecisionFormat.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-getshaderprecisionformat - * @name Access to webgl rendering context getShaderPrecisionFormat - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.getShaderPrecisionFormat - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("getShaderPrecisionFormat") -select api, "getShaderPrecisionFormat is an indicator of fingerprinting, weighed 750.75 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_getSupportedExtensions.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_getSupportedExtensions.ql deleted file mode 100644 index 6c6cf48555d..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_getSupportedExtensions.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-getsupportedextensions - * @name Access to webgl rendering context getSupportedExtensions - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.getSupportedExtensions - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("getSupportedExtensions") -select api, "getSupportedExtensions is an indicator of fingerprinting, weighed 1058.69 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_webgl_RenderingContext_readPixels.ql b/.github/codeql/queries/autogen_webgl_RenderingContext_readPixels.ql deleted file mode 100644 index efd2e9515ae..00000000000 --- a/.github/codeql/queries/autogen_webgl_RenderingContext_readPixels.ql +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @id prebid/webgl-readpixels - * @name Access to webgl rendering context readPixels - * @kind problem - * @problem.severity warning - * @description Finds uses of webgl RenderingContext.readPixels - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("webgl") and - api = invocation.getAPropertyRead("readPixels") -select api, "readPixels is an indicator of fingerprinting, weighed 20.37 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_devicePixelRatio.ql b/.github/codeql/queries/autogen_window_devicePixelRatio.ql deleted file mode 100644 index 7d6cca21576..00000000000 --- a/.github/codeql/queries/autogen_window_devicePixelRatio.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-devicepixelratio - * @name Access to window.devicePixelRatio - * @kind problem - * @problem.severity warning - * @description Finds uses of window.devicePixelRatio - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("devicePixelRatio") -select api, "devicePixelRatio is an indicator of fingerprinting, weighed 18.68 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_indexedDB.ql b/.github/codeql/queries/autogen_window_indexedDB.ql deleted file mode 100644 index 4ce36d5a518..00000000000 --- a/.github/codeql/queries/autogen_window_indexedDB.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-indexeddb - * @name Access to window.indexedDB - * @kind problem - * @problem.severity warning - * @description Finds uses of window.indexedDB - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("indexedDB") -select api, "indexedDB is an indicator of fingerprinting, weighed 18.13 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_openDatabase.ql b/.github/codeql/queries/autogen_window_openDatabase.ql deleted file mode 100644 index d90f5b491c0..00000000000 --- a/.github/codeql/queries/autogen_window_openDatabase.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-opendatabase - * @name Access to window.openDatabase - * @kind problem - * @problem.severity warning - * @description Finds uses of window.openDatabase - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("openDatabase") -select api, "openDatabase is an indicator of fingerprinting, weighed 139.85 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_outerHeight.ql b/.github/codeql/queries/autogen_window_outerHeight.ql deleted file mode 100644 index 4c4b64e9439..00000000000 --- a/.github/codeql/queries/autogen_window_outerHeight.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-outerheight - * @name Access to window.outerHeight - * @kind problem - * @problem.severity warning - * @description Finds uses of window.outerHeight - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("outerHeight") -select api, "outerHeight is an indicator of fingerprinting, weighed 200.62 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_outerWidth.ql b/.github/codeql/queries/autogen_window_outerWidth.ql deleted file mode 100644 index 4313c7cc937..00000000000 --- a/.github/codeql/queries/autogen_window_outerWidth.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-outerwidth - * @name Access to window.outerWidth - * @kind problem - * @problem.severity warning - * @description Finds uses of window.outerWidth - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("outerWidth") -select api, "outerWidth is an indicator of fingerprinting, weighed 107.23 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_screenLeft.ql b/.github/codeql/queries/autogen_window_screenLeft.ql deleted file mode 100644 index 0e39d072655..00000000000 --- a/.github/codeql/queries/autogen_window_screenLeft.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-screenleft - * @name Access to window.screenLeft - * @kind problem - * @problem.severity warning - * @description Finds uses of window.screenLeft - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("screenLeft") -select api, "screenLeft is an indicator of fingerprinting, weighed 286.83 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_screenTop.ql b/.github/codeql/queries/autogen_window_screenTop.ql deleted file mode 100644 index 517da52b3bf..00000000000 --- a/.github/codeql/queries/autogen_window_screenTop.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-screentop - * @name Access to window.screenTop - * @kind problem - * @problem.severity warning - * @description Finds uses of window.screenTop - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("screenTop") -select api, "screenTop is an indicator of fingerprinting, weighed 286.72 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_screenX.ql b/.github/codeql/queries/autogen_window_screenX.ql deleted file mode 100644 index 24e4702696b..00000000000 --- a/.github/codeql/queries/autogen_window_screenX.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-screenx - * @name Access to window.screenX - * @kind problem - * @problem.severity warning - * @description Finds uses of window.screenX - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("screenX") -select api, "screenX is an indicator of fingerprinting, weighed 337.97 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/autogen_window_screenY.ql b/.github/codeql/queries/autogen_window_screenY.ql deleted file mode 100644 index 23675d4d8d6..00000000000 --- a/.github/codeql/queries/autogen_window_screenY.ql +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @id prebid/window-screeny - * @name Access to window.screenY - * @kind problem - * @problem.severity warning - * @description Finds uses of window.screenY - */ - -// this file is autogenerated, see fingerprintApis.mjs - -import prebid -from SourceNode api -where - api = windowPropertyRead("screenY") -select api, "screenY is an indicator of fingerprinting, weighed 324.59 in https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json" diff --git a/.github/codeql/queries/fpEventProperty.ql b/.github/codeql/queries/fpEventProperty.ql new file mode 100644 index 00000000000..38a79c5bad8 --- /dev/null +++ b/.github/codeql/queries/fpEventProperty.ql @@ -0,0 +1,58 @@ +/** + * @id prebid/fp-event-property + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds property access on event objects (e.g. `.addEventListener('someEvent', (event) => event.someProperty)`) + +import prebid +import autogen_fpEventProperty + +/* + Tracks event objects through `addEventListener` + (1st argument to the 2nd argument passed to an `addEventListener`) +*/ +SourceNode eventListener(TypeTracker t, string event) { + t.start() and + ( + exists(MethodCallNode addEventListener | + addEventListener.getMethodName() = "addEventListener" and + addEventListener.getArgument(0).mayHaveStringValue(event) and + result = addEventListener.getArgument(1).(FunctionNode).getParameter(0) + ) + ) + or + exists(TypeTracker t2 | + result = eventListener(t2, event).track(t2, t) + ) +} + +/* + Tracks event objects through 'onevent' property assignments + (1st argument of the assignment's right hand) +*/ +SourceNode eventSetter(TypeTracker t, string eventSetter) { + t.start() and + exists(PropWrite write | + write.getPropertyName() = eventSetter and + result = write.getRhs().(FunctionNode).getParameter(0) + ) or + exists(TypeTracker t2 | + result = eventSetter(t2, eventSetter).track(t2, t) + ) +} + +bindingset[event] +SourceNode event(string event) { + result = eventListener(TypeTracker::end(), event) or + result = eventSetter(TypeTracker::end(), "on" + event.toLowerCase()) +} + + +from EventProperty prop, SourceNode use +where + use = event(prop.getEvent()).getAPropertyRead(prop) +select use, prop.getEvent() + "event ." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpGlobalConstructors.ql b/.github/codeql/queries/fpGlobalConstructors.ql new file mode 100644 index 00000000000..8e73aa473a0 --- /dev/null +++ b/.github/codeql/queries/fpGlobalConstructors.ql @@ -0,0 +1,17 @@ +/** + * @id prebid/fp-global-constructors + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds uses of global constructors (e.g. `new SomeConstructor()`) + +import prebid +import autogen_fpGlobalConstructor + +from GlobalConstructor ctor, SourceNode use +where + use = callTo(global(ctor)) +select use, ctor + " is an indicator of fingerprinting; weight: " + ctor.getWeight() diff --git a/.github/codeql/queries/fpGlobalVariable.ql b/.github/codeql/queries/fpGlobalVariable.ql new file mode 100644 index 00000000000..7e21c5198e8 --- /dev/null +++ b/.github/codeql/queries/fpGlobalVariable.ql @@ -0,0 +1,18 @@ +/** + * @id prebid/fp-global-var + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds use of global variables (e.g. `someVariable`) + +import prebid +import autogen_fpGlobalVar + + +from GlobalVar var, SourceNode use +where + use = windowPropertyRead(var) +select use, var + " is an indicator of fingerprinting; weight: " + var.getWeight() diff --git a/.github/codeql/queries/fpMethod.ql b/.github/codeql/queries/fpMethod.ql new file mode 100644 index 00000000000..5b212cd336a --- /dev/null +++ b/.github/codeql/queries/fpMethod.ql @@ -0,0 +1,19 @@ +/** + * @id prebid/fp-method + * @name Possible use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds calls to a given method name (e.g. object.someMethod()) + +import prebid +import autogen_fpDOMMethod + + +from DOMMethod meth, MethodCallNode use +where + use.getMethodName() = meth + // there's no easy way to check the method call is on the right type +select use, meth + " is an indicator of fingerprinting if used on " + meth.getType() +"; weight: " + meth.getWeight() diff --git a/.github/codeql/queries/fpOneDeepObjectProperty.ql b/.github/codeql/queries/fpOneDeepObjectProperty.ql new file mode 100644 index 00000000000..d89db520d35 --- /dev/null +++ b/.github/codeql/queries/fpOneDeepObjectProperty.ql @@ -0,0 +1,17 @@ +/** + * @id prebid/fp-one-deep-object-prop + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting +*/ + +// Finds property access on instances of objects reachable 1 level down from a global (e.g. `someName.someObject.someProperty`) + +import prebid +import autogen_fpGlobalObjectProperty1 + +from GlobalObjectProperty1 prop, SourceNode use +where + use = oneDeepGlobal(prop.getGlobal0(), prop.getGlobal1()).getAPropertyRead(prop) +select use, prop.getGlobal0() + "." + prop.getGlobal1() + "." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpOneDeepTypeProperty.ql b/.github/codeql/queries/fpOneDeepTypeProperty.ql new file mode 100644 index 00000000000..6603df74e2b --- /dev/null +++ b/.github/codeql/queries/fpOneDeepTypeProperty.ql @@ -0,0 +1,26 @@ +/** + * @id prebid/fp-one-deep-constructor-prop + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting +*/ + +// Finds property access on instances of types reachable 1 level down from a global (e.g. `new SomeName.SomeType().someProperty`) + +import prebid +import autogen_fpGlobalTypeProperty1 + +SourceNode oneDeepType(TypeTracker t, string parent, string ctor) { + t.start() and ( + result = callTo(oneDeepGlobal(parent, ctor)) + ) or exists(TypeTracker t2 | + result = oneDeepType(t2, parent, ctor).track(t2, t) + ) +} + + +from GlobalTypeProperty1 prop, SourceNode use +where + use = oneDeepType(TypeTracker::end(), prop.getGlobal0(), prop.getGlobal1()).getAPropertyRead(prop) +select use, prop.getGlobal0() + "." + prop.getGlobal1() + "." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpRenderingContextProperty.ql b/.github/codeql/queries/fpRenderingContextProperty.ql new file mode 100644 index 00000000000..f2411457c06 --- /dev/null +++ b/.github/codeql/queries/fpRenderingContextProperty.ql @@ -0,0 +1,30 @@ +/** + * @id prebid/fp-rendering-context-property + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds use of rendering context properties (e.g. canvas.getContext().someProperty) + +import prebid +import autogen_fpRenderingContextProperty + +/* + Tracks objects returned by a call to `.getContext()` +*/ +SourceNode renderingContext(TypeTracker t, string contextType) { + t.start() and exists(MethodCallNode invocation | + invocation.getMethodName() = "getContext" and + invocation.getArgument(0).mayHaveStringValue(contextType) and + result = invocation + ) or exists(TypeTracker t2 | + result = renderingContext(t2, contextType).track(t2, t) + ) +} + +from RenderingContextProperty prop, SourceNode use +where + use = renderingContext(TypeTracker::end(), prop.getContextType()).getAPropertyRead(prop) +select use, "canvas.getContext()." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpSensorProperty.ql b/.github/codeql/queries/fpSensorProperty.ql new file mode 100644 index 00000000000..ce210e93d24 --- /dev/null +++ b/.github/codeql/queries/fpSensorProperty.ql @@ -0,0 +1,36 @@ +/** + * @id prebid/fp-sensor-property + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds property access on sensor objects (e.g. `new Gyroscope().someProperty`) + +import prebid +import autogen_fpSensorProperty + +SourceNode sensor(TypeTracker t) { + t.start() and exists(string variant | + variant in [ + // Sensor subtypes, https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs + "Gyroscope", + "Accelerometer", + "GravitySensor", + "LinearAccelerationSensor", + "AbsoluteOrientationSensor", + "RelativeOrientationSensor", + "Magnetometer", + "AmbientLightSensor" + ] and + result = callTo(global(variant)) + ) or exists(TypeTracker t2 | + result = sensor(t2).track(t2, t) + ) +} + +from SensorProperty prop, SourceNode use +where + use = sensor(TypeTracker::end()).getAPropertyRead(prop) +select use, "Sensor." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpTopLevelObjectProperty.ql b/.github/codeql/queries/fpTopLevelObjectProperty.ql new file mode 100644 index 00000000000..b5e270feb42 --- /dev/null +++ b/.github/codeql/queries/fpTopLevelObjectProperty.ql @@ -0,0 +1,17 @@ +/** + * @id prebid/fp-top-level-object-prop + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds property access on top-level global objects (e.g. `someObject.someProperty`) + +import prebid +import autogen_fpGlobalObjectProperty0 + +from GlobalObjectProperty0 prop, SourceNode use +where + use = global(prop.getGlobal0()).getAPropertyRead(prop) +select use, prop.getGlobal0() + "." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/fpTopLevelTypeProperty.ql b/.github/codeql/queries/fpTopLevelTypeProperty.ql new file mode 100644 index 00000000000..2eae7b2af53 --- /dev/null +++ b/.github/codeql/queries/fpTopLevelTypeProperty.ql @@ -0,0 +1,26 @@ +/** + * @id prebid/fp-top-level-type-prop + * @name Use of browser API associated with fingerprinting + * @kind problem + * @problem.severity warning + * @description Usage of browser APIs associated with fingerprinting + */ + +// Finds property access on instances of top-level types (e.g. `new SomeType().someProperty`) + +import prebid +import autogen_fpGlobalTypeProperty0 + +SourceNode topLevelType(TypeTracker t, string ctor) { + t.start() and ( + result = callTo(global(ctor)) + ) or exists(TypeTracker t2 | + result = topLevelType(t2, ctor).track(t2, t) + ) +} + + +from GlobalTypeProperty0 prop, SourceNode use +where + use = topLevelType(TypeTracker::end(), prop.getGlobal0()).getAPropertyRead(prop) +select use, prop.getGlobal0() + "." + prop + " is an indicator of fingerprinting; weight: " + prop.getWeight() diff --git a/.github/codeql/queries/prebid.qll b/.github/codeql/queries/prebid.qll index 0bcf68cfba0..bb9c6e50080 100644 --- a/.github/codeql/queries/prebid.qll +++ b/.github/codeql/queries/prebid.qll @@ -1,20 +1,39 @@ import javascript import DataFlow +SourceNode otherWindow(TypeTracker t) { + t.start() and ( + result = globalVarRef("window") or + result = globalVarRef("top") or + result = globalVarRef("self") or + result = globalVarRef("parent") or + result = globalVarRef("frames").getAPropertyRead() or + result = DOM::documentRef().getAPropertyRead("defaultView") + ) or + exists(TypeTracker t2 | + result = otherWindow(t2).track(t2, t) + ) +} + SourceNode otherWindow() { - result = globalVarRef("top") or - result = globalVarRef("self") or - result = globalVarRef("parent") or - result = globalVarRef("frames").getAPropertyRead() or - result = DOM::documentRef().getAPropertyRead("defaultView") + result = otherWindow(TypeTracker::end()) +} + +SourceNode connectedWindow(TypeTracker t, SourceNode win) { + t.start() and ( + result = win.getAPropertyRead("self") or + result = win.getAPropertyRead("top") or + result = win.getAPropertyRead("parent") or + result = win.getAPropertyRead("frames").getAPropertyRead() or + result = win.getAPropertyRead("document").getAPropertyRead("defaultView") + ) or + exists(TypeTracker t2 | + result = connectedWindow(t2, win).track(t2, t) + ) } SourceNode connectedWindow(SourceNode win) { - result = win.getAPropertyRead("self") or - result = win.getAPropertyRead("top") or - result = win.getAPropertyRead("parent") or - result = win.getAPropertyRead("frames").getAPropertyRead() or - result = win.getAPropertyRead("document").getAPropertyRead("defaultView") + result = connectedWindow(TypeTracker::end(), win) } SourceNode relatedWindow(SourceNode win) { @@ -27,15 +46,58 @@ SourceNode anyWindow() { result = relatedWindow(otherWindow()) } +SourceNode windowPropertyRead(TypeTracker t, string prop) { + t.start() and ( + result = globalVarRef(prop) or + result = anyWindow().getAPropertyRead(prop) + ) or + exists(TypeTracker t2 | + result = windowPropertyRead(t2, prop).track(t2, t) + ) +} + /* Matches uses of property `prop` done on any window object. */ SourceNode windowPropertyRead(string prop) { - result = globalVarRef(prop) or - result = anyWindow().getAPropertyRead(prop) + result = windowPropertyRead(TypeTracker::end(), prop) +} + +/** + Matches both invocations and instantiations of fn. +*/ +SourceNode callTo(SourceNode fn) { + result = fn.getAnInstantiation() or + result = fn.getAnInvocation() } -SourceNode instantiationOf(string ctr) { - result = windowPropertyRead(ctr).getAnInstantiation() +SourceNode global(TypeTracker t, string name) { + t.start() and ( + result = windowPropertyRead(name) + ) or exists(TypeTracker t2 | + result = global(t2, name).track(t2, t) + ) } + +/** + Tracks a global (name reachable from a window object). +*/ +SourceNode global(string name) { + result = global(TypeTracker::end(), name) +} + +SourceNode oneDeepGlobal(TypeTracker t, string parent, string name) { + t.start() and ( + result = global(parent).getAPropertyRead(name) + ) or exists(TypeTracker t2 | + result = oneDeepGlobal(t2, parent, name).track(t2, t) + ) +} + +/* + Tracks a name reachable 1 level down from the global (e.g. `Intl.DateTimeFormat`). +*/ +SourceNode oneDeepGlobal(string parent, string name) { + result = oneDeepGlobal(TypeTracker::end(), parent, name) +} diff --git a/.github/codeql/queries/sensor.qll b/.github/codeql/queries/sensor.qll new file mode 100644 index 00000000000..d2d56606cd6 --- /dev/null +++ b/.github/codeql/queries/sensor.qll @@ -0,0 +1,22 @@ +import prebid + +SourceNode sensor(TypeTracker t) { + t.start() and exists(string variant | + variant in [ + "Gyroscope", + "Accelerometer", + "LinearAccelerationSensor", + "AbsoluteOrientationSensor", + "RelativeOrientationSensor", + "Magnetometer", + "AmbientLightSensor" + ] and + result = callTo(variant) + ) or exists(TypeTracker t2 | + result = sensor(t2).track(t2, t) + ) +} + +SourceNode sensor() { + result = sensor(TypeTracker::end()) +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e626632d1ef..007ba6d26b4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,12 +3,33 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" - package-ecosystem: "npm" directory: "/" + target-branch: "dependabotTarget" schedule: - interval: "weekly" + interval: "quarterly" + open-pull-requests-limit: 2 versioning-strategy: increase + allow: + - dependency-name: 'iab-adcom' + - dependency-name: 'iab-native' + - dependency-name: 'iab-openrtb' + - dependency-name: '@types/*' + - dependency-name: '@eslint/compat' + - dependency-name: 'eslint' + - dependency-name: '@babel/*' + - dependency-name: 'webpack' ignore: - dependency-name: "*" update-types: ["version-update:semver-major"] + - package-ecosystem: "npm" + directory: "/" + target-branch: "master" + schedule: + interval: "daily" + open-pull-requests-limit: 0 + groups: + all-security: + applies-to: security-updates + patterns: ["*"] diff --git a/.github/workflows/PR-assignment.yml b/.github/workflows/PR-assignment.yml new file mode 100644 index 00000000000..5bac9b5d763 --- /dev/null +++ b/.github/workflows/PR-assignment.yml @@ -0,0 +1,60 @@ +name: Assign PR reviewers +on: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + assign_reviewers: + name: Assign reviewers + runs-on: ubuntu-latest + + steps: + - name: Generate app token + id: token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.PR_BOT_ID }} + private-key: ${{ secrets.PR_BOT_PEM }} + + - name: Checkout + uses: actions/checkout@v5 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/head + + - name: Install dependencies + uses: ./.github/actions/npm-ci + - name: Build + run: | + npx gulp build + - name: Install s3 client + run: | + npm install @aws-sdk/client-s3 + - name: Get PR properties + id: get-props + uses: actions/github-script@v8 + env: + AWS_ACCESS_KEY_ID: ${{ vars.PR_BOT_AWS_AK }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.PR_BOT_AWS_SAK }} + with: + github-token: ${{ steps.token.outputs.token }} + script: | + const getProps = require('./.github/workflows/scripts/getPRProperties.js') + const props = await getProps({ + github, + context, + prNo: ${{ github.event.pull_request.number }}, + reviewerTeam: '${{ vars.REVIEWER_TEAM }}', + engTeam: '${{ vars.ENG_TEAM }}', + authReviewTeam: '${{ vars.AUTH_REVIEWER_TEAM }}' + }); + console.log('PR properties:', JSON.stringify(props, null, 2)); + return props; + - name: Assign reviewers + if: ${{ !fromJSON(steps.get-props.outputs.result).review.ok }} + uses: actions/github-script@v8 + with: + github-token: ${{ steps.token.outputs.token }} + script: | + const assignReviewers = require('./.github/workflows/scripts/assignReviewers.js') + const reviewers = await assignReviewers({github, context, prData: ${{ steps.get-props.outputs.result }} }); + console.log('Assigned reviewers:', JSON.stringify(reviewers, null, 2)); diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 00000000000..3e4bc947ad1 --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,133 @@ +name: Run unit tests on all browsers +on: + workflow_call: + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.unit-tests.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" +jobs: + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: npx gulp build + + setup: + needs: build + name: "Define testing strategy" + runs-on: ubuntu-latest + outputs: + browsers: ${{ toJSON(fromJSON(steps.define.outputs.result).browsers) }} + latestBrowsers: ${{ toJSON(fromJSON(steps.define.outputs.result).latestBrowsers) }} + bstack-key: ${{ steps.bstack-save.outputs.name }} + bstack-sessions: ${{ fromJSON(steps.define.outputs.result).bsBrowsers }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Restore working directory + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + - name: "Define testing strategy" + uses: actions/github-script@v8 + id: define + with: + script: | + const fs = require('node:fs/promises'); + const browsers = Object.entries( + require('./.github/workflows/browser_testing.json') + ).flatMap(([name, browser]) => { + browser = Object.assign({name, version: 'latest'}, browser); + const browsers = [browser]; + const versions = browser.versions; + if (versions) { + delete browser.versions; + browsers.push(...Object.entries(versions).map(([version, def]) => Object.assign({}, browser, {version, ...def}))) + } + return browsers; + }) + const bstackBrowsers = Object.fromEntries( + // exclude versions of browsers that we can test on GH actions + Object.entries(require('./browsers.json')) + .filter(([name, def]) => browsers.find(({bsName, version}) => bsName === def.browser && version === def.browser_version) == null) + ) + const updatedBrowsersJson = JSON.stringify(bstackBrowsers, null, 2); + console.log("Using browsers.json:", updatedBrowsersJson); + console.log("Browsers to be tested directly on runners:", JSON.stringify(browsers, null, 2)) + await fs.writeFile('./browsers.json', updatedBrowsersJson); + return { + bsBrowsers: Object.keys(bstackBrowsers).length, + browsers, + latestBrowsers: browsers.filter(browser => browser.version === 'latest') + } + - name: "Save working directory" + id: bstack-save + if: ${{ fromJSON(steps.define.outputs.result).bsBrowsers > 0 }} + uses: ./.github/actions/save + with: + prefix: browserstack- + + test-build-logic: + needs: build + name: "Test build logic" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: gulp test-build-logic + + e2e-tests: + needs: [setup, build] + name: "E2E (browser: ${{ matrix.browser.wdioName }})" + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.setup.outputs.browsers) }} + uses: + ./.github/workflows/run-tests.yml + with: + browser: ${{ matrix.browser.wdioName }} + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: npx gulp e2e-test-nobuild --local + chunks: 1 + runs-on: ${{ matrix.browser.runsOn || 'ubuntu-latest' }} + install-safari: ${{ matrix.browser.runsOn == 'macos-latest' }} + run-npm-install: ${{ matrix.browser.runsOn == 'windows-latest' }} + browserstack: false + + unit-tests: + needs: [setup, build] + name: "Unit (browser: ${{ matrix.browser.name }} ${{ matrix.browser.version }})" + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.setup.outputs.browsers) }} + uses: + ./.github/workflows/run-tests.yml + with: + install-deb: ${{ matrix.browser.deb }} + install-chrome: ${{ matrix.browser.chrome }} + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: npx gulp test-only-nobuild --browsers ${{ matrix.browser.name }} ${{ matrix.browser.coverage && '--coverage' || '--no-coverage' }} + chunks: 8 + runs-on: ${{ matrix.browser.runsOn || 'ubuntu-latest' }} + + browserstack-tests: + needs: setup + if: ${{ needs.setup.outputs.bstack-key }} + name: "Browserstack tests" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.setup.outputs.bstack-key }} + test-cmd: npx gulp test-only-nobuild --browserstack --no-coverage + chunks: 8 + browserstack: true + browserstack-sessions: ${{ fromJSON(needs.setup.outputs.bstack-sessions) }} + secrets: + BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/browser_testing.json b/.github/workflows/browser_testing.json new file mode 100644 index 00000000000..46b0894e071 --- /dev/null +++ b/.github/workflows/browser_testing.json @@ -0,0 +1,28 @@ +{ + "ChromeHeadless": { + "bsName": "chrome", + "wdioName": "chrome", + "coverage": true, + "versions": { + "113.0": { + "coverage": false, + "chrome": "113.0.5672.0", + "name": "ChromeNoSandbox" + } + } + }, + "EdgeHeadless": { + "bsName": "edge", + "wdioName": "msedge", + "runsOn": "windows-latest" + }, + "SafariNative": { + "wdioName": "safari technology preview", + "bsName": "safari", + "runsOn": "macos-latest" + }, + "FirefoxHeadless": { + "wdioName": "firefox", + "bsName": "firefox" + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000..6942d25b54c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Run unit tests +on: + workflow_call: + inputs: + source-key: + description: Artifact name for source directory + type: string + required: false + default: source + build-cmd: + description: Build command + required: false + type: string + outputs: + built-key: + description: Artifact name for built directory + value: ${{ jobs.build.outputs.built-key }} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + built-key: ${{ inputs.build-cmd && steps.save.outputs.name || inputs.source-key }} + steps: + - name: Checkout + if: ${{ inputs.build-cmd }} + uses: actions/checkout@v5 + - name: Restore source + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/load + with: + name: ${{ inputs.source-key }} + - name: Build + if: ${{ inputs.build-cmd }} + run: ${{ inputs.build-cmd }} + - name: 'Save working directory' + id: save + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/save + with: + prefix: 'build-' diff --git a/.github/workflows/code-path-changes.yml b/.github/workflows/code-path-changes.yml index 5927aae1ada..8d327a7e2b1 100644 --- a/.github/workflows/code-path-changes.yml +++ b/.github/workflows/code-path-changes.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v5 - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '18' diff --git a/.github/workflows/code-reviewer-assignment.yml b/.github/workflows/code-reviewer-assignment.yml deleted file mode 100644 index dd7aed2c930..00000000000 --- a/.github/workflows/code-reviewer-assignment.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: Rule Based Reviewer Assignment - -on: - pull_request_target: - types: [opened] - -permissions: - pull-requests: write - contents: read - -jobs: - assign_reviewers: - runs-on: ubuntu-latest - - steps: - - name: Checkout base repo - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.base.ref }} - fetch-depth: 2 - - - name: Get PR Author and Files Changed - id: pr-info - run: | - # Get PR author - echo "author=${{ github.event.pull_request.user.login }}" >> $GITHUB_ENV - - FILES_CHANGED=$(GH_TOKEN="${{ secrets.PAT_CODE_REVIEWER_AUTOMATION }}" gh api \ - repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files \ - --jq '.[].filename') - - CORE_FILES_CHANGED="false" - for FILE in $FILES_CHANGED; do - echo "$FILE" - if [[ ! "$FILE" =~ ^modules/[^/]+$ && ! "$FILE" =~ ^test/ && ! "$FILE" =~ ^integrationExamples/ ]]; then - CORE_FILES_CHANGED="true" - echo "Found a core change" - break - fi - done - - echo "core_change=$CORE_FILES_CHANGED" >> $GITHUB_ENV - - - name: Assign Reviewers Based on Rules - run: | - # Load PR author and core change flag - AUTHOR=${{ env.author }} - CORE_CHANGE=${{ env.core_change }} - echo "PR Author: $AUTHOR" - echo "Core Change: $CORE_CHANGE" - - # Define groups - PREBID_LEAD_ENG=("dgirardi") - PREBID_ENG=("mkomorski") - VOLUNTEERS=($(GH_TOKEN="${{ secrets.PAT_CODE_REVIEWER_AUTOMATION }}" gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - /orgs/prebid/teams/pbjs-reviewers/members \ - --jq '.[].login')) - MEMBERS=("3link" "abazylewicz-id5" "Abyfall" "adserver-online" "aleksatr" "alexander-kislitsyn" "AlexBVolcy" "AlexisBRENON" "alexsavelyev" "anastasiiapankivFS" "And1sS" "andre-gielow-ttd" "andreacastello" "andrewmarriott-aws" "andyblackwell" "ankit-thanekar007" "AntoxaAntoxic" "apukh-magnite" "arielmtk" "armando-fs" "AvinashKapre" "bbaresic" "BenBoonsiri" "bjorn-lw" "bokelley" "bretg" "bsardo" "Bugxyb" "bwnodak" "bwschmidt" "carlosfelix" "cciocov" "ccorbo" "chicoman25" "Compile-Ninja" "CTMBNara" "danielsao" "dbemiller" "dbridges12" "decaffeinatedio" "deepthivenkat" "el-chuck" "EmilNadimanov" "Enigo" "EvgeniiMunin" "farukcam" "fatihkaya84" "Fawke" "fliccione" "FlorentDancy" "florianerl" "freestarjonny" "Fuska1" "gargcreation1992" "Gershon-Brainin" "gilbertococchi" "github-matthieu-wipliez" "github-mickael-leclerc" "github-richard-depierre" "gmcgrath11" "gmiedlar-ox" "gpolaert" "guscarreon" "gwhigs" "harpere" "harrykingriches" "headertag" "heatherboveri" "hhhjort" "hjeong12" "ianwow" "idettman" "ikp4success" "IrinLen" "jaiminpanchal27" "jclou" "jdcauley" "jdelhommeau" "jdwieland8282" "jefftmahoney" "jeremy-greenbids" "jerrycychen" "JimTharioAmazon" "jlaso" "jlquaccia" "jlukas79" "jlustig11" "jney" "joedrew" "JoelPM" "johnwier" "JonGoSonobi" "jsnellbaker" "jsut" "justadreamer" "jwrosewell" "kamermans" "kapil-tuptewar" "katherynhrabik" "khang-vu-ttd" "kim-ng93" "kiril-kalchev" "kkharma" "kvnsw" "laurb9" "lcorrigall" "linux019" "lksharma" "lpagnypubstack" "lucor" "MaksymTeqBlaze" "mansinahar" "marki1an" "matthewlane" "MaxSmileWanted" "mbellomi" "mercuryyy" "michachen" "Miroku87" "mkendall07" "mmoschovas" "mmullin" "monis0395" "monisq" "muuki88" "mwilsonmagnite" "nassimlounadi" "ncolletti" "Net-burst" "nhedley" "nicgallardo" "nickllerandi" "NikhilGopalChennissery" "OlenaPostindustria" "ollyburns" "omerDotan" "onkarvhanumante" "optidigital-prebid" "oronno" "osazos" "osulzhenko" "ourcraig" "passani" "patmmccann" "paulborile" "pb-pete" "pdamoc" "peixunzhang" "piotrj-rtbh" "pkowalski-id5" "pm-abhinav-deshpande" "pm-asit-sahoo" "pm-azhar-mulla" "pm-harshad-mane" "pm-isha-bharti" "pm-jaydeep-mohite" "pm-kapil-tuptewar" "pm-komal-kumari" "pm-manasi-moghe" "pm-nikhil-vaidya" "pm-nitin-nimbalkar" "pm-nitin-shirsat" "pm-priyanka-bagade" "pm-priyanka-deshmane" "pm-saurabh-narkhede" "pm-shivam-soni" "pm-tanishka-vishwakarma" "pm-viral-vala" "Pratik3307" "protonate" "Pubmatic-Dhruv-Sonone" "PubMatic-OpenWrap" "Pubmatic-Supriya-Patil" "PyjamaWarrior" "QuentinGallard" "rBeefrz" "richmtk" "rickyblaha" "rimaburder-index" "rishi-parmar" "rmloveland" "robertrmartinez" "schernysh" "scr-oath" "sebastienrufiange" "sebmil-daily" "sergseven" "shahinrahbariasl" "ShriprasadM" "sigma-software-prebid" "SKOCHERI" "smenzer" "snapwich" "softcoder594" "sonali-more-xandr" "ssundahlTTD" "StavBenShlomoBrowsi" "stephane-ein" "teads-antoine-azar" "tej656" "teqblaze-yurii" "thyagram-aws" "ValentinPostindustria" "VeronikaSolovei9" "vivekyadav15" "vkimcm" "vraybaud" "wi101" "yq-yang-qin" "ysfbsf" "YuriyVelichkoPI" "yuva-inmobi-1" "zapo" "zhongshixi" "zxPhoenix") - - # Helpers - pick_random_from_group() { - local group=("$@") - echo "${group[$RANDOM % ${#group[@]}]}" - } - - pick_random_from_group_excluding() { - local excludes_str="$1" - shift - local group=("$@") - IFS=" " read -r -a excludes <<< "$excludes_str" - - local filtered=() - for user in "${group[@]}"; do - local skip=false - for ex in "${excludes[@]}"; do - if [[ "$user" == "$ex" ]]; then - skip=true - break - fi - done - if [[ "$skip" == false ]]; then - filtered+=("$user") - fi - done - - if [[ ${#filtered[@]} -eq 0 ]]; then - echo "" - else - echo "${filtered[$RANDOM % ${#filtered[@]}]}" - fi - } - - REVIEWERS=() - - if [[ " ${PREBID_LEAD_ENG[@]} " =~ " ${AUTHOR} " ]]; then - # Prebid Lead authored --> 2 Reviewers (Non-Lead Prebid + Volunteer) - echo "Prebid Lead engineer authored the PR" - REVIEWERS+=("$(pick_random_from_group "${PREBID_ENG[@]}")") - REVIEWERS+=("$(pick_random_from_group "${VOLUNTEERS[@]}")") - elif [[ " ${PREBID_ENG[@]} " =~ " ${AUTHOR} " ]]; then - echo "Prebid engineer authored the PR" - # Any other Prebid engineer authored --> 2 Reviewers (Lead Prebid + Volunteer) - REVIEWERS+=("${PREBID_LEAD_ENG[0]}") - REVIEWERS+=("$(pick_random_from_group "${VOLUNTEERS[@]}")") - elif [[ "$CORE_CHANGE" == "true" ]]; then - # Core rules apply to anyone else --> 2 Reviewers (Lead Prebid + Volunteer) - echo "Core change detected, applying core rules" - REVIEWERS+=("${PREBID_LEAD_ENG[0]}") - REVIEWERS+=("$(pick_random_from_group_excluding "$AUTHOR" "${VOLUNTEERS[@]}")") - elif [[ " ${MEMBERS[@]} " =~ " ${AUTHOR} " ]]; then - echo "Non-core, member authored" - # Non-core, member authored --> 1 Reviewer (Non-Lead Prebid) - REVIEWERS+=("$(pick_random_from_group "${PREBID_ENG[@]}")") - else - echo "Non-core, non-member authored" - # Non-core, non-member authored --> 1 Reviewer (Volunteer) - REVIEWERS+=("$(pick_random_from_group_excluding "$AUTHOR" "${VOLUNTEERS[@]}")") - fi - - echo "Reviewers selected: ${REVIEWERS[@]}" - - # Assign reviewers using gh api - for R in "${REVIEWERS[@]}"; do - if [[ -n "$R" ]]; then - echo "Assigning reviewer: $R" - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/requested_reviewers \ - -f reviewers[]="$R" - fi - done - - env: - GITHUB_TOKEN: ${{ secrets.PAT_CODE_REVIEWER_AUTOMATION }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index aaeb89e9815..f86cd38a43c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -70,4 +70,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/jscpd.yml b/.github/workflows/jscpd.yml index 5fc6e48291f..39e54bebcf0 100644 --- a/.github/workflows/jscpd.yml +++ b/.github/workflows/jscpd.yml @@ -17,7 +17,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '20' diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5ef3998307b..30d327d3495 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '20' diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a14e12664b6..53d458a3948 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,6 +5,7 @@ on: # branches to consider in the event; optional, defaults to all branches: - master + - '*.x-legacy' permissions: contents: read diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 00000000000..74105f7a921 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,237 @@ +name: Run unit tests +on: + workflow_call: + inputs: + browser: + description: values to set as the BROWSER env variable + required: false + type: string + chunks: + description: Number of chunks to split tests into + required: false + type: number + default: 1 + build-cmd: + description: Build command, run once + required: false + type: string + built-key: + description: Artifact name for built source + required: false + type: string + test-cmd: + description: Test command, run once per chunk + required: true + type: string + browserstack: + description: If true, set up browserstack environment and adjust concurrency + required: false + type: boolean + default: false + browserstack-sessions: + description: Number of browserstack sessions needed to run tests + required: false + type: number + default: 6 + timeout: + description: Timeout on test run + required: false + type: number + default: 10 + runs-on: + description: Runner image + required: false + default: ubuntu-latest + type: string + install-safari: + description: Install Safari + type: boolean + required: false + default: false + install-chrome: + description: Chrome version to install via @puppeteer/browsers + type: string + required: false + run-npm-install: + description: Run npm install before tests + type: boolean + required: false + default: false + install-deb: + description: URL to deb to install before tests + type: string + required: false + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.collect-coverage.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" + +jobs: + checkout: + name: "Define chunks" + runs-on: ubuntu-latest + outputs: + chunks: ${{ steps.chunks.outputs.chunks }} + id: ${{ steps.chunks.outputs.id }} + steps: + - name: Define chunks + id: chunks + run: | + echo 'chunks=[ '$(seq --separator=, 1 1 ${{ inputs.chunks }})' ]' >> out; + echo 'id='"$(uuidgen)" >> out; + cat out >> "$GITHUB_OUTPUT"; + + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: ${{ !inputs.built-key && inputs.build-cmd || '' }} + source-key: ${{ inputs.built-key || 'source' }} + + run-tests: + needs: [checkout, build] + strategy: + fail-fast: false + max-parallel: ${{ inputs.browserstack && 1 || inputs.chunks }} + matrix: + chunk-no: ${{ fromJSON(needs.checkout.outputs.chunks) }} + + name: Test${{ inputs.chunks > 1 && format(' chunk {0}', matrix.chunk-no) || '' }} + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + TEST_CHUNKS: ${{ inputs.chunks }} + TEST_CHUNK: ${{ matrix.chunk-no }} + BROWSER: ${{ inputs.browser }} + outputs: + coverage: ${{ steps.coverage.outputs.coverage }} + concurrency: + # The following generates 'browserstack-' when inputs.browserstack is true, and a hopefully unique ID otherwise + # Ideally we'd like to serialize browserstack access across all workflows, but github's max queue length is only 1 + # (cfr. https://github.com/orgs/community/discussions/12835) + # so we add the run_id to serialize only within one push / pull request (which has the effect of queueing e2e and unit tests) + group: ${{ inputs.browserstack && format('browserstack-{0}', github.run_id) || format('{0}-{1}', needs.checkout.outputs.id, matrix.chunk-no) }} + cancel-in-progress: false + + runs-on: ${{ inputs.runs-on }} + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + + - name: Install safari + if: ${{ inputs.install-safari }} + run: | + brew install --cask safari-technology-preview + defaults write com.apple.Safari IncludeDevelopMenu YES + defaults write com.apple.Safari AllowRemoteAutomation 1 + sudo safaridriver --enable + sudo "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver" --enable + + - name: Install Chrome + if: ${{ inputs.install-chrome }} + shell: bash + run: | + out=($(npx @puppeteer/browsers install chrome@${{ inputs.install-chrome }})) + echo 'CHROME_BIN='"${out[1]}" >> env; + cat env + cat env >> "$GITHUB_ENV" + + - name: Install deb + if: ${{ inputs.install-deb }} + uses: ./.github/actions/install-deb + with: + url: ${{ inputs.install-deb }} + + - name: Run npm install + if: ${{ inputs.run-npm-install }} + run: | + npm install + + - name: 'BrowserStack Env Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-env@master' + with: + username: ${{ secrets.BROWSERSTACK_USER_NAME}} + access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + build-name: Run ${{github.run_id}}, attempt ${{ github.run_attempt }}, chunk ${{ matrix.chunk-no }}, ref ${{ github.event_name == 'pull_request_target' && format('PR {0}', github.event.pull_request.number) || github.ref }}, ${{ inputs.test-cmd }} + + - name: 'BrowserStackLocal Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: start + local-identifier: random + + - name: 'Wait for browserstack' + if: ${{ inputs.browserstack }} + uses: ./.github/actions/wait-for-browserstack + with: + sessions: ${{ inputs.browserstack-sessions }} + + - name: Run tests + uses: nick-fields/retry@v3 + with: + timeout_minutes: ${{ inputs.timeout }} + max_attempts: 3 + command: ${{ inputs.test-cmd }} + shell: bash + + - name: 'BrowserStackLocal Stop' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: stop + + - name: 'Check for coverage' + id: 'coverage' + shell: bash + run: | + if [ -d "./build/coverage" ]; then + echo 'coverage=true' >> "$GITHUB_OUTPUT"; + fi + + - name: 'Save coverage result' + if: ${{ steps.coverage.outputs.coverage }} + uses: actions/upload-artifact@v4 + with: + name: coverage-partial-${{inputs.test-cmd}}-${{ matrix.chunk-no }} + path: ./build/coverage + overwrite: true + + collect-coverage: + if: ${{ needs.run-tests.outputs.coverage }} + needs: [build, run-tests] + name: 'Collect coverage results' + runs-on: ubuntu-latest + outputs: + coverage: coverage-complete-${{ inputs.test-cmd }} + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + + - name: Download coverage results + uses: actions/download-artifact@v6 + with: + path: ./build/coverage + pattern: coverage-partial-${{ inputs.test-cmd }}-* + merge-multiple: true + + - name: 'Save working directory' + uses: ./.github/actions/save + with: + name: coverage-complete-${{ inputs.test-cmd }} + diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml deleted file mode 100644 index 3089f588554..00000000000 --- a/.github/workflows/run-unit-tests.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Run unit tests -on: - workflow_call: - inputs: - build-cmd: - description: Build command, run once - required: true - type: string - test-cmd: - description: Test command, run once per chunk - required: true - type: string - serialize: - description: If true, allow only one concurrent chunk (see note on concurrency below) - required: false - type: boolean - outputs: - wdir: - description: Cache key for the working directory after running tests - value: ${{ jobs.chunk-4.outputs.wdir }} - secrets: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: '20' - - - name: Fetch source - uses: actions/cache/restore@v4 - with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true - - - name: Build - run: ${{ inputs.build-cmd }} - - - name: Cache build output - uses: actions/cache/save@v4 - with: - path: . - key: build-${{ inputs.build-cmd }}-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v4 - with: - path: . - key: build-${{ inputs.build-cmd }}-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true - - chunk-1: - needs: build - name: Run tests (chunk 1 of 4) - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 1 - wdir: build-${{ inputs.build-cmd }}-${{ github.run_id }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-2: - name: Run tests (chunk 2 of 4) - needs: chunk-1 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 2 - wdir: ${{ needs.chunk-1.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-3: - name: Run tests (chunk 3 of 4) - needs: chunk-2 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 3 - wdir: ${{ needs.chunk-2.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-4: - name: Run tests (chunk 4 of 4) - needs: chunk-3 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 4 - wdir: ${{ needs.chunk-3.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/scripts/assignReviewers.js b/.github/workflows/scripts/assignReviewers.js new file mode 100644 index 00000000000..8bbe50f6104 --- /dev/null +++ b/.github/workflows/scripts/assignReviewers.js @@ -0,0 +1,39 @@ +const ghRequester = require('./ghRequest.js'); + +function pickFrom(candidates, exclude, no) { + exclude = exclude.slice(); + const winners = []; + while (winners.length < no) { + const candidate = candidates[Math.floor(Math.random() * candidates.length)]; + if (!exclude.includes(candidate)) { + winners.push(candidate); + exclude.push(candidate); + } + } + return winners; +} + +async function assignReviewers({github, context, prData}) { + const allReviewers = prData.review.reviewers.map(rv => rv.login); + const requestedReviewers = prData.review.requestedReviewers; + const missingPrebidEng = prData.review.requires.prebidEngineers - prData.review.prebidEngineers; + const missingPrebidReviewers = prData.review.requires.prebidReviewers - prData.review.prebidReviewers - (missingPrebidEng > 0 ? missingPrebidEng : 0); + + if (missingPrebidEng > 0) { + requestedReviewers.push(...pickFrom(prData.prebidEngineers, [...allReviewers, prData.author.login], missingPrebidEng)) + } + if (missingPrebidReviewers > 0) { + requestedReviewers.push(...pickFrom(prData.prebidReviewers, [...allReviewers, prData.author.login], missingPrebidReviewers)) + } + + const request = ghRequester(github); + await request('POST /repos/{owner}/{repo}/pulls/{prNo}/requested_reviewers', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo: prData.pr, + reviewers: requestedReviewers + }) + return requestedReviewers; +} + +module.exports = assignReviewers; diff --git a/.github/workflows/scripts/getPRProperties.js b/.github/workflows/scripts/getPRProperties.js new file mode 100644 index 00000000000..f663e976ce5 --- /dev/null +++ b/.github/workflows/scripts/getPRProperties.js @@ -0,0 +1,163 @@ +const ghRequester = require('./ghRequest.js'); +const AWS = require("@aws-sdk/client-s3"); + +const MODULE_PATTERNS = [ + /^modules\/([^\/]+)BidAdapter(\.(\w+)|\/)/, + /^modules\/([^\/]+)AnalyticsAdapter(\.(\w+)|\/)/, + /^modules\/([^\/]+)RtdProvider(\.(\w+)|\/)/, + /^modules\/([^\/]+)IdSystem(\.(\w+)|\/)/ +] + +const EXCLUDE_PATTERNS = [ + /^test\//, + /^integrationExamples\// +] +const LIBRARY_PATTERN = /^libraries\/([^\/]+)\//; + +function extractVendor(chunkName) { + for (const pat of MODULE_PATTERNS) { + const match = pat.exec(`modules/${chunkName}`); + if (match != null) { + return match[1]; + } + } + return chunkName; +} + +const getLibraryRefs = (() => { + const deps = require('../../../build/dist/dependencies.json'); + const refs = {}; + return function (libraryName) { + if (!refs.hasOwnProperty(libraryName)) { + refs[libraryName] = new Set(); + Object.entries(deps) + .filter(([name, deps]) => deps.includes(`${libraryName}.js`)) + .forEach(([name]) => refs[libraryName].add(extractVendor(name))) + } + return refs[libraryName]; + } +})(); + +function isCoreFile(path) { + if (EXCLUDE_PATTERNS.find(pat => pat.test(path))) { + return false; + } + if (MODULE_PATTERNS.find(pat => pat.test(path)) ) { + return false; + } + const lib = LIBRARY_PATTERN.exec(path); + if (lib != null) { + // a library is "core" if it's used by more than one vendor + return getLibraryRefs(lib[1]).size > 1; + } + return true; +} + +async function isPrebidMember(ghHandle) { + const client = new AWS.S3({region: 'us-east-2'}); + const res = await client.getObject({ + Bucket: 'repo-dashboard-files-891377123989', + Key: 'memberMapping.json' + }); + const members = JSON.parse(await res.Body.transformToString()); + return members.includes(ghHandle); +} + + +async function getPRProperties({github, context, prNo, reviewerTeam, engTeam, authReviewTeam}) { + const request = ghRequester(github); + let [files, pr, prReviews, prebidReviewers, prebidEngineers, authorizedReviewers] = await Promise.all([ + request('GET /repos/{owner}/{repo}/pulls/{prNo}/files', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + request('GET /repos/{owner}/{repo}/pulls/{prNo}', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + request('GET /repos/{owner}/{repo}/pulls/{prNo}/reviews', { + owner: context.repo.owner, + repo: context.repo.repo, + prNo, + }), + ...[reviewerTeam, engTeam, authReviewTeam].map(team => request('GET /orgs/{org}/teams/{team}/members', { + org: context.repo.owner, + team, + })) + ]); + prebidReviewers = prebidReviewers.data.map(datum => datum.login); + prebidEngineers = prebidEngineers.data.map(datum=> datum.login); + authorizedReviewers = authorizedReviewers.data.map(datum=> datum.login); + let isCoreChange = false; + files = files.data.map(datum => datum.filename).map(file => { + const core = isCoreFile(file); + if (core) isCoreChange = true; + return { + file, + core + } + }); + const review = { + prebidEngineers: 0, + prebidReviewers: 0, + reviewers: [], + requestedReviewers: [] + }; + const author = pr.data.user.login; + const allReviewers = new Set(); + pr.data.requested_reviewers + .forEach(rv => { + allReviewers.add(rv.login); + review.requestedReviewers.push(rv.login); + }); + prReviews.data.forEach(datum => allReviewers.add(datum.user.login)); + + allReviewers + .forEach(reviewer => { + if (reviewer === author) return; + const isPrebidEngineer = prebidEngineers.includes(reviewer); + const isPrebidReviewer = isPrebidEngineer || prebidReviewers.includes(reviewer) || authorizedReviewers.includes(reviewer); + if (isPrebidEngineer) { + review.prebidEngineers += 1; + } + if (isPrebidReviewer) { + review.prebidReviewers += 1 + } + review.reviewers.push({ + login: reviewer, + isPrebidEngineer, + isPrebidReviewer, + }) + }); + const data = { + pr: prNo, + author: { + login: author, + isPrebidMember: await isPrebidMember(author) + }, + isCoreChange, + files, + prebidReviewers, + prebidEngineers, + review, + } + data.review.requires = reviewRequirements(data); + data.review.ok = satisfiesReviewRequirements(data.review); + return data; +} + +function reviewRequirements(prData) { + return { + prebidEngineers: prData.author.isPrebidMember ? 1 : 0, + prebidReviewers: prData.isCoreChange ? 2 : 1 + } +} + +function satisfiesReviewRequirements({requires, prebidEngineers, prebidReviewers}) { + return prebidEngineers >= requires.prebidEngineers && prebidReviewers >= requires.prebidReviewers +} + + +module.exports = getPRProperties; diff --git a/.github/workflows/scripts/ghRequest.js b/.github/workflows/scripts/ghRequest.js new file mode 100644 index 00000000000..cc09edaf390 --- /dev/null +++ b/.github/workflows/scripts/ghRequest.js @@ -0,0 +1,9 @@ +module.exports = function githubRequester(github) { + return function (verb, params) { + return github.request(verb, Object.assign({ + headers: { + 'X-GitHub-Api-Version': '2022-11-28' + } + }, params)) + } +} diff --git a/.github/workflows/test-chunk.yml b/.github/workflows/test-chunk.yml deleted file mode 100644 index faba70b16a7..00000000000 --- a/.github/workflows/test-chunk.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Test chunk -on: - workflow_call: - inputs: - serialize: - required: false - type: boolean - cmd: - required: true - type: string - chunk-no: - required: true - type: number - wdir: - required: true - type: string - outputs: - wdir: - description: "Cache key for the working directory after running tests" - value: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - secrets: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" - -concurrency: - # The following generates 'browserstack-' when inputs.serialize is true, and a hopefully unique ID otherwise - # Ideally we'd like to serialize browserstack access across all workflows, but github's max queue length is only 1 - # (cfr. https://github.com/orgs/community/discussions/12835) - # so we add the run_id to serialize only within one push / pull request (which has the effect of queueing e2e and unit tests) - group: ${{ inputs.serialize && 'browser' || github.run_id }}${{ inputs.serialize && 'stack' || inputs.cmd }}-${{ github.run_id }} - cancel-in-progress: false - -jobs: - test: - name: "Test chunk ${{ inputs.chunk-no }}" - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - TEST_CHUNKS: 4 - TEST_CHUNK: ${{ inputs.chunk-no }} - runs-on: ubuntu-latest - steps: - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: '20' - - - name: Restore working directory - id: restore-dir - uses: actions/cache/restore@v4 - with: - path: . - key: ${{ inputs.wdir }} - fail-on-cache-miss: true - - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 8 - max_attempts: 1 - command: ${{ inputs.cmd }} - - - name: Save working directory - uses: actions/cache/save@v4 - with: - path: . - key: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v4 - with: - path: . - key: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true - - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5415d9e678..c56b8cae4c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: base-commit: ${{ steps.info.outputs.base-commit }} steps: - name: Set up Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '20' @@ -53,37 +53,24 @@ jobs: echo base-commit="${{ github.event.pull_request.base.sha || github.event.before }}" >> $GITHUB_OUTPUT - name: Install dependencies - run: npm ci + uses: ./.github/actions/npm-ci - - name: Cache source - uses: actions/cache/save@v4 + - name: 'Save working directory' + uses: ./.github/actions/save with: - path: . - key: source-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v4 - with: - path: . - key: source-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true + name: source lint: name: "Run linter" needs: checkout runs-on: ubuntu-latest steps: - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: '20' + - name: Checkout + uses: actions/checkout@v5 - name: Restore source - uses: actions/cache/restore@v4 + uses: ./.github/actions/load with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true + name: source - name: lint run: | npx eslint @@ -91,65 +78,36 @@ jobs: test-no-features: name: "Unit tests (all features disabled)" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml + uses: ./.github/workflows/run-tests.yml with: + chunks: 8 build-cmd: npx gulp precompile-all-features-disabled test-cmd: npx gulp test-all-features-disabled-nobuild - serialize: false + browserstack: false secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} test: - name: "Unit tests (all features enabled + coverage)" + name: "Browser tests" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml - with: - build-cmd: npx gulp precompile - test-cmd: npx gulp test-only-nobuild --browserstack - serialize: true + uses: ./.github/workflows/browser-tests.yml secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - test-e2e: - name: "End-to-end tests" - needs: checkout - runs-on: ubuntu-latest - concurrency: - # see test-chunk.yml for notes on concurrency groups - group: browserstack-${{ github.run_id }} - cancel-in-progress: false - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - steps: - - name: Set up Node.js - uses: actions/setup-node@v5 - with: - node-version: '20' - - name: Restore source - uses: actions/cache/restore@v4 - with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 10 - max_attempts: 3 - command: npx gulp e2e-test coveralls: name: Update coveralls needs: [checkout, test] runs-on: ubuntu-latest steps: - - name: Restore working directory - uses: actions/cache/restore@v4 + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load with: - path: . - key: ${{ needs.test.outputs.wdir }} - fail-on-cache-miss: true + name: ${{ needs.test.outputs.coverage }} + - name: Coveralls uses: coverallsapp/github-action@v2 with: diff --git a/browsers.json b/browsers.json index 974df030ee7..f37d708221e 100644 --- a/browsers.json +++ b/browsers.json @@ -15,11 +15,11 @@ "device": null, "os": "Windows" }, - "bs_chrome_109_windows_10": { + "bs_chrome_113_windows_10": { "base": "BrowserStack", "os_version": "10", "browser": "chrome", - "browser_version": "109.0", + "browser_version": "113.0", "device": null, "os": "Windows" }, @@ -47,5 +47,4 @@ "device": null, "os": "OS X" } - } diff --git a/customize/buildOptions.mjs b/customize/buildOptions.mjs index 7b8013ab269..3341f35b0e0 100644 --- a/customize/buildOptions.mjs +++ b/customize/buildOptions.mjs @@ -1,8 +1,12 @@ import path from 'path' -import validate from 'schema-utils' +import { validate } from 'schema-utils' const boModule = path.resolve(import.meta.dirname, '../dist/src/buildOptions.mjs') +/** + * Resolve the absolute path of the default build options module. + * @returns {string} Absolute path to the generated build options module. + */ export function getBuildOptionsModule () { return boModule } @@ -25,6 +29,11 @@ const schema = { } } +/** + * Validate and load build options overrides. + * @param {object} [options] user supplied overrides + * @returns {Promise} Promise resolving to merged build options. + */ export function getBuildOptions (options = {}) { validate(schema, options, { name: 'Prebid build options', diff --git a/fingerprintApis.mjs b/fingerprintApis.mjs index 4694042923b..0f130b0fd2b 100644 --- a/fingerprintApis.mjs +++ b/fingerprintApis.mjs @@ -1,11 +1,19 @@ +/** + * Implementation of `gulp update-codeql`; + * this fetches duckduckgo's "fingerprinting score" of browser APIs + * and generates codeQL classes (essentially data tables) containing info about + * the APIs, used by codeQL queries to scan for their usage in the codebase. + */ + import _ from 'lodash'; + const weightsUrl = `https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json`; import fs from 'fs/promises'; import path from 'path'; const MIN_WEIGHT = 15; const QUERY_DIR = path.join(import.meta.dirname, '.github/codeql/queries'); -const QUERY_FILE_PREFIX = 'autogen_'; +const TYPE_FILE_PREFIX = 'autogen_'; async function fetchWeights() { const weights = await fetch(weightsUrl).then(res => res.json()); @@ -14,176 +22,213 @@ async function fetchWeights() { ); } -const QUERY_FILE_TPL = _.template(`/** - * @id prebid/<%= id %> - * @name <%= name %> - * @kind problem - * @problem.severity warning - * @description <%= description %> +const TUPLE_TPL = _.template(`// this file is autogenerated, see fingerprintApis.mjs + +class <%= name %> extends string { +<% fields.forEach(([type, name]) => {%> + <%= type %> <%= name %>; <%}) %> + + <%= name %>() {<% values.forEach((vals, i) => { %> + <% if(i > 0) {%> or <% }%> + (<% vals.forEach(([name, value], j) => { %> <% if(j > 0) {%> and <% }%><%= name %> = <%= value %><%})%> )<%})%> + } +<% fields.forEach(([type, name]) => {%> + <%= type %> get<%= name.charAt(0).toUpperCase() + name.substring(1) %>() { + result = <%= name %> + } + <% }) %> +} +`); + +/** + * Generate source for a codeQL class containing a set of API names and other data + * that may be necessary to query for them. + * + * The generated type has at least + * + * "string this" set to the API name, + * "float weight" set to the fingerprinting weight. + * + * @param name Class name + * @param fields Additional fields, provided a list of [type, name] pairs. + * @param values A list of values each provided as a [weight, ...fieldValues, apiName] tuple. + * `fieldValues` must map onto `fields`. */ - -// this file is autogenerated, see fingerprintApis.mjs +function makeTupleType(name, fields, values) { + const quote = (val) => typeof val === 'string' ? `"${val}"` : val; + fields.unshift(['float', 'weight']); + values = values.map((vals) => { + return [ + ['this', quote(vals.pop())], + ...fields.map(([_, name], i) => ([name, quote(vals[i])])) + ] + }) + return [ + name, + TUPLE_TPL({ + name, fields, values + }) + ]; +} -<%= query %> -`); +/** + * Global names - no other metadata necessary + */ +function globalConstructor(matches) { + return makeTupleType('GlobalConstructor', [], matches) +} -function message(weight, api) { - return `"${api} is an indicator of fingerprinting, weighed ${weight} in ${weightsUrl}"` +/** + * Global names - no other metadata necessary + */ +function globalVar(matches) { + return makeTupleType( + 'GlobalVar', + [], + matches + ); } -function windowProp(weight, api) { - return [ - `window_${api}`, - QUERY_FILE_TPL({ - id: `window-${api}`.toLowerCase(), - name: `Access to window.${api}`, - description: `Finds uses of window.${api}`, - query: `import prebid -from SourceNode api -where - api = windowPropertyRead("${api}") -select api, ${message(weight, api)}` - }) - ] + +/** + * Property of some globally available type. + * this = property name + * global0...globalN: names used to reach the type from the global; last is the type itself. + * e.g. `Intl.DateTimeFormat` has global0 = 'Intl', global1 = 'DateTimeFormat'. + */ +function globalTypeProperty(depth = 0) { + const fields = Array.from(Array(depth + 1).keys()).map(i => (['string', `global${i}`])) + return function (matches) { + return makeTupleType(`GlobalTypeProperty${depth}`, fields, matches) + } } -function globalProp(prop) { - return function (weight, api) { - return [ - `${prop}_${api}`, - QUERY_FILE_TPL({ - id: `${prop}-${api}`.toLowerCase(), - name: `Access to ${prop}.${api}`, - description: `Finds uses of ${prop}.${api}`, - query: `import prebid -from SourceNode prop, SourceNode api -where - prop = windowPropertyRead("${prop}") and - api = prop.getAPropertyRead("${api}") -select api, ${message(weight, api)}` - }) - ] +/** + * Property of some globally available object. + * this = property name + * global0...globalN: path to reach the object (as in globalTypeProperty) + */ +function globalObjectProperty(depth, getPath) { + const fields = Array.from(Array(depth + 1).keys()).map((i) => (['string', `global${i}`])) + return function (matches) { + return makeTupleType( + `GlobalObjectProperty${depth}`, + fields, + matches.map(([weight, ...values]) => [weight, ...getPath(values), values[values.length - 1]]) + ) } } -function globalConstructor(weight, ctr) { - return [ - `${ctr}`, - QUERY_FILE_TPL({ - id: `${ctr}`.toLowerCase(), - name: `Use of ${ctr}`, - description: `Finds uses of ${ctr}`, - query: `import prebid -from SourceNode api -where - api = instantiationOf("${ctr}") -select api, ${message(weight, ctr)}` - }) - ] +/** + * Property of a canvas' RenderingContext. + * + * this = property name + * contextType = the argument passed to `getContext`. + */ +function renderingContextProperty(matches) { + const fields = [['string', 'contextType']]; + const contextMap = { + 'WebGLRenderingContext': 'webgl', + 'WebGL2RenderingContext': 'webgl2', + 'CanvasRenderingContext2D': '2d' + } + matches = matches.map(([weight, contextType, prop]) => [ + weight, contextMap[contextType], prop + ]); + return makeTupleType('RenderingContextProperty', fields, matches); } -function globalConstructorProperty(weight, ctr, api) { - return [ - `${ctr}_${api}`, - QUERY_FILE_TPL({ - id: `${ctr}-${api}`.toLowerCase(), - name: `Access to ${ctr}.${api}`, - description: `Finds uses of ${ctr}.${api}`, - query: `import prebid -from SourceNode inst, SourceNode api -where - inst = instantiationOf("${ctr}") and - api = inst.getAPropertyRead("${api}") -select api, ${message(weight, api)}` - }) - ] +/** + * Property of an event object. + * + * this = property name + * event = event type + */ +function eventProperty(matches) { + const fields = [['string', 'event']]; + const eventMap = { + 'RTCPeerConnectionIce': 'icecandidate' + } + matches = matches.map(([weight, eventType, prop]) => [weight, eventMap[eventType] ?? eventType.toLowerCase(), prop]) + return makeTupleType('EventProperty', fields, matches); } -function simplePropertyMatch(weight, target, prop) { - return [ - `${target}_${prop}`, - QUERY_FILE_TPL({ - id: `${target}-${prop}`.toLowerCase(), - name: `Potential access to ${target}.${prop}`, - description: `Finds uses of ${prop}`, - query: `import prebid -from PropRef api -where - api.getPropertyName() = "${prop}" -select api, ${message(weight, prop)}` - }) - ] +/** + * Property of a sensor object. + */ +function sensorProperty(matches) { + return makeTupleType('SensorProperty', [], matches); } -function glContextMatcher(contextType) { - return function(weight, api) { - return [ - `${contextType}_RenderingContext_${api}`, - QUERY_FILE_TPL({ - id: `${contextType}-${api}`.toLowerCase(), - name: `Access to ${contextType} rendering context ${api}`, - description: `Finds uses of ${contextType} RenderingContext.${api}`, - query: `import prebid -from InvokeNode invocation, SourceNode api -where - invocation.getCalleeName() = "getContext" and - invocation.getArgument(0).mayHaveStringValue("${contextType}") and - api = invocation.getAPropertyRead("${api}") -select api, ${message(weight, api)}` - }) - ] - } + +/** + * Method of some type. + * this = method name + * type = prototype name + */ +function domMethod(matches) { + return makeTupleType('DOMMethod', [['string', 'type']], matches) } const API_MATCHERS = [ - [/^([^.]+)\.prototype.constructor$/, globalConstructor], - [/^Screen\.prototype\.(.*)$/, globalProp('screen')], - [/^Notification\.([^.]+)$/, globalProp('Notification')], - [/^window\.(.*)$/, windowProp], - [/^Navigator.prototype\.(.*)$/, globalProp('navigator')], - [/^(Date|Gyroscope)\.prototype\.(.*)$/, globalConstructorProperty], - [/^(DeviceMotionEvent)\.prototype\.(.*)$/, simplePropertyMatch], - [/^WebGLRenderingContext\.prototype\.(.*)$/, glContextMatcher('webgl')], - [/^WebGL2RenderingContext\.prototype\.(.*)$/, glContextMatcher('webgl2')], - [/^CanvasRenderingContext2D\.prototype\.(.*)$/, glContextMatcher('2d')], + [/^([^.]+)\.prototype.constructor$/, globalConstructor], + [/^(Date|Gyroscope)\.prototype\.(.*)$/, globalTypeProperty()], + [/^(Intl)\.(DateTimeFormat)\.prototype\.(.*)$/, globalTypeProperty(1)], + [/^(Screen\.prototype|Notification|Navigator\.prototype)\.(.*)$/, globalObjectProperty(0, + ([name]) => ({ + 'Screen.prototype': ['screen'], + 'Notification': ['Notification'], + 'Navigator.prototype': ['navigator'] + }[name]) + )], + [/^window\.(.*)$/, globalVar], + [/^(WebGL2?RenderingContext|CanvasRenderingContext2D)\.prototype\.(.*)$/, renderingContextProperty], + [/^(DeviceOrientation|DeviceMotion|RTCPeerConnectionIce)Event\.prototype\.(.*)$/, eventProperty], + [/^MediaDevices\.prototype\.(.*)$/, globalObjectProperty(1, () => ['navigator', 'mediaDevices'])], + [/^Sensor.prototype\.(.*)$/, sensorProperty], + [/^(HTMLCanvasElement|AudioBuffer)\.prototype\.(toDataURL|getChannelData)/, domMethod], ]; -async function generateQueries() { +async function generateTypes() { const weights = await fetchWeights(); - const queries = {}; + const matches = new Map(); Object.entries(weights).filter(([identifier, weight]) => { for (const [matcher, queryGen] of API_MATCHERS) { const match = matcher.exec(identifier); if (match) { - const [name, query] = queryGen(weight, ...match.slice(1)); - queries[name] = query; + if (!matches.has(matcher)) { + matches.set(matcher, [queryGen, []]); + } + matches.get(matcher)[1].push([weight, ...match.slice(1)]); delete weights[identifier]; break; } } - }) - const unmatched = Object.keys(weights); + }); if (Object.keys(weights).length > 0) { - console.warn(`The following APIs are weighed more than ${MIN_WEIGHT}, but no query was generated for them:`, JSON.stringify(weights, null, 2)) + console.warn(`The following APIs are weighed more than ${MIN_WEIGHT}, but no types were generated for them:`, JSON.stringify(weights, null, 2)); } - return queries; + return Object.fromEntries( + Array.from(matches.values()) + .map(([queryGen, matches]) => queryGen(matches)) + ); } async function clearFiles() { for (const file of await fs.readdir(QUERY_DIR)) { - if (file.startsWith(QUERY_FILE_PREFIX)) { - await fs.rm(path.join(QUERY_DIR, file)) + if (file.startsWith(TYPE_FILE_PREFIX)) { + await fs.rm(path.join(QUERY_DIR, file)); } } } -async function generateQueryFiles() { - for (const [name, query] of Object.entries(await generateQueries())) { - await fs.writeFile(path.join(QUERY_DIR, `autogen_${name}.ql`), query); +async function generateTypeFiles() { + for (const [name, query] of Object.entries(await generateTypes())) { + await fs.writeFile(path.join(QUERY_DIR, `autogen_fp${name}.qll`), query); } } export async function updateQueries() { await clearFiles(); - await generateQueryFiles(); + await generateTypeFiles(); } diff --git a/gulpfile.js b/gulpfile.js index dc1cc51f62e..e5a4db41884 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,7 +39,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const {precompile, babelPrecomp} = require('./gulp.precompilation.js'); -const TEST_CHUNKS = 4; +const TEST_CHUNKS = 8; // these modules must be explicitly listed in --modules to be included in the build, won't be part of "all" modules var explicitModules = [ @@ -521,7 +521,7 @@ gulp.task('build-bundle-verbose', gulp.series(precompile(), makeWebpackPkg(makeV // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('update-browserslist', execaTask('npx update-browserslist-db@latest')); gulp.task('test-build-logic', execaTask('npx mocha ./test/build-logic')) -gulp.task('test-only-nobuild', gulp.series('test-build-logic', testTaskMaker({coverage: true}))) +gulp.task('test-only-nobuild', gulp.series(testTaskMaker({coverage: argv.coverage ?? true}))) gulp.task('test-only', gulp.series('test-build-logic', 'precompile', test)); gulp.task('test-all-features-disabled-nobuild', testTaskMaker({disableFeatures: helpers.getTestDisableFeatures(), oneBrowser: 'chrome', watch: false})); @@ -554,6 +554,7 @@ gulp.task('default', gulp.series('build')); gulp.task('e2e-test-only', gulp.series(requireNodeVersion(16), () => runWebdriver({file: argv.file}))); gulp.task('e2e-test', gulp.series(requireNodeVersion(16), clean, 'build-bundle-prod', e2eTestTaskMaker())); +gulp.task('e2e-test-nobuild', gulp.series(requireNodeVersion(16), e2eTestTaskMaker())) // other tasks gulp.task(bundleToStdout); diff --git a/integrationExamples/gpt/neuwoRtdProvider_example.html b/integrationExamples/gpt/neuwoRtdProvider_example.html index d0f6005c623..3d6fef98995 100644 --- a/integrationExamples/gpt/neuwoRtdProvider_example.html +++ b/integrationExamples/gpt/neuwoRtdProvider_example.html @@ -1,205 +1,384 @@ - - - - - - - - + + + - googletag.cmd.push(function() { - googletag.defineSlot('/19968336/header-bid-tag-0', div_1_sizes, 'div-1').addService(googletag.pubads()); - googletag.pubads().enableSingleRequest(); - googletag.enableServices(); - }); - googletag.cmd.push(function() { - googletag.defineSlot('/19968336/header-bid-tag-1', div_2_sizes, 'div-2').addService(googletag.pubads()); - googletag.pubads().enableSingleRequest(); - googletag.enableServices(); - }); - - - -

Basic Prebid.js Example using neuwoRtdProvider

-
- Looks like you're not following the testing environment setup, try accessing http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html - after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js - - npm ci - npm i -g gulp-cli - gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter - + +

Basic Prebid.js Example using Neuwo Rtd Provider

+ +
+ Looks like you're not following the testing environment setup, try accessing + + http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html + + after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js + + // Install dependencies + npm ci + + // Run a local development server + npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter + + // No tests + npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter + + // Only tests + npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/neuwoRtdProvider_spec.js + +
+ +
+

Neuwo Rtd Provider Configuration

+

Add token and url to use for Neuwo extension configuration

+
+
-
-

Add token and url to use for Neuwo extension configuration

- - - - +
+ +
+
+
-
Div-1
-
- Ad spot div-1: This content will be replaced by prebid.js and/or related components once you click "Update" +

IAB Content Taxonomy Options

+
+
-
+

Cache Options

+
+ +
-
Div-2
-
- Ad spot div-2: Replaces this text as well, if everything goes to plan - - +

URL Cleaning Options

+
+ +
+
+ +
+
+ +
+
+
- - + +
+ +
+

Ad Examples

+ +
+

Div-1

+
+ + Ad spot div-1: This content will be replaced by prebid.js and/or related components once you click + "Update" and there are no errors. +
+
+ +
+

Div-2

+
+ + Ad spot div-2: This content will be replaced by prebid.js and/or related components once you click + "Update" and there are no errors. +
+
+
+ +
+

Neuwo Data in Bid Request

+

The retrieved data from Neuwo API is injected into the bid request as OpenRTB (ORTB2)`site.content.data` and + `user.data`. Full bid request can be inspected in Developer Tools Console under + INFO: NeuwoRTDModule injectIabCategories: post-injection bidsConfig +

+
+ + + + + + + \ No newline at end of file diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index f247dd6d565..ff37eed3ead 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -83,9 +83,10 @@

Prebid Test Bidder Example

+

+

Banner ad
- \ No newline at end of file + diff --git a/karma.conf.maker.js b/karma.conf.maker.js index ce7110def58..f6f9d903d58 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -40,6 +40,7 @@ function newWebpackConfig(codeCoverage, disableFeatures) { function newPluginsArray(browserstack) { var plugins = [ 'karma-chrome-launcher', + 'karma-safarinative-launcher', 'karma-coverage', 'karma-mocha', 'karma-chai', @@ -47,14 +48,14 @@ function newPluginsArray(browserstack) { 'karma-sourcemap-loader', 'karma-spec-reporter', 'karma-webpack', - 'karma-mocha-reporter' + 'karma-mocha-reporter', + '@chiragrupani/karma-chromium-edge-launcher', ]; if (browserstack) { plugins.push('karma-browserstack-launcher'); } plugins.push('karma-firefox-launcher'); plugins.push('karma-opera-launcher'); - plugins.push('karma-safari-launcher'); plugins.push('karma-script-launcher'); return plugins; } @@ -84,13 +85,19 @@ function setReporters(karmaConf, codeCoverage, browserstack, chunkNo) { } function setBrowsers(karmaConf, browserstack) { + karmaConf.customLaunchers = karmaConf.customLaunchers || {}; + karmaConf.customLaunchers.ChromeNoSandbox = { + base: 'ChromeHeadless', + // disable sandbox - necessary within Docker and when using versions installed through @puppeteer/browsers + flags: ['--no-sandbox'] + } if (browserstack) { karmaConf.browserStack = { username: process.env.BROWSERSTACK_USERNAME, accessKey: process.env.BROWSERSTACK_ACCESS_KEY, - build: 'Prebidjs Unit Tests ' + new Date().toLocaleString() + build: process.env.BROWSERSTACK_BUILD_NAME } - if (process.env.TRAVIS) { + if (process.env.BROWSERSTACK_LOCAL_IDENTIFIER) { karmaConf.browserStack.startTunnel = false; karmaConf.browserStack.tunnelIdentifier = process.env.BROWSERSTACK_LOCAL_IDENTIFIER; } @@ -99,14 +106,7 @@ function setBrowsers(karmaConf, browserstack) { } else { var isDocker = require('is-docker')(); if (isDocker) { - karmaConf.customLaunchers = karmaConf.customLaunchers || {}; - karmaConf.customLaunchers.ChromeCustom = { - base: 'ChromeHeadless', - // We must disable the Chrome sandbox when running Chrome inside Docker (Chrome's sandbox needs - // more permissions than Docker allows by default) - flags: ['--no-sandbox'] - } - karmaConf.browsers = ['ChromeCustom']; + karmaConf.browsers = ['ChromeNoSandbox']; } else { karmaConf.browsers = ['ChromeHeadless']; } @@ -174,10 +174,10 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: !watchMode, - browserDisconnectTimeout: 1e5, // default 2000 - browserNoActivityTimeout: 1e5, // default 10000 - captureTimeout: 3e5, // default 60000, - browserDisconnectTolerance: 1, + browserDisconnectTimeout: 1e4, + browserNoActivityTimeout: 3e4, + captureTimeout: 2e4, + browserDisconnectTolerance: 5, concurrency: 5, // browserstack allows us 5 concurrent sessions plugins: plugins diff --git a/libraries/adagioUtils/adagioUtils.js b/libraries/adagioUtils/adagioUtils.js index c2614c45d0c..265a442017a 100644 --- a/libraries/adagioUtils/adagioUtils.js +++ b/libraries/adagioUtils/adagioUtils.js @@ -22,6 +22,7 @@ export const _ADAGIO = (function() { const w = getBestWindowForAdagio(); w.ADAGIO = w.ADAGIO || {}; + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; diff --git a/libraries/advangUtils/index.js b/libraries/advangUtils/index.js index ac99f3c71d3..7d869cef9e1 100644 --- a/libraries/advangUtils/index.js +++ b/libraries/advangUtils/index.js @@ -1,5 +1,5 @@ import { generateUUID, isFn, parseSizesInput, parseUrl } from '../../src/utils.js'; -import { getDNT as getNavigatorDNT } from '../navigatorData/dnt.js'; +import { getDNT as getNavigatorDNT } from '../dnt/index.js'; import { config } from '../../src/config.js'; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; diff --git a/libraries/cmp/cmpEventUtils.ts b/libraries/cmp/cmpEventUtils.ts new file mode 100644 index 00000000000..4619e9605c9 --- /dev/null +++ b/libraries/cmp/cmpEventUtils.ts @@ -0,0 +1,121 @@ +/** + * Shared utilities for CMP event listener management + * Used by TCF and GPP consent management modules + */ + +import { logError, logInfo } from "../../src/utils.js"; + +export interface CmpEventManager { + cmpApi: any; + listenerId: number | undefined; + setCmpApi(cmpApi: any): void; + getCmpApi(): any; + setCmpListenerId(listenerId: number | undefined): void; + getCmpListenerId(): number | undefined; + removeCmpEventListener(): void; + resetCmpApis(): void; +} + +/** + * Base CMP event manager implementation + */ +export abstract class BaseCmpEventManager implements CmpEventManager { + cmpApi: any = null; + listenerId: number | undefined = undefined; + + setCmpApi(cmpApi: any): void { + this.cmpApi = cmpApi; + } + + getCmpApi(): any { + return this.cmpApi; + } + + setCmpListenerId(listenerId: number | undefined): void { + this.listenerId = listenerId; + } + + getCmpListenerId(): number | undefined { + return this.listenerId; + } + + resetCmpApis(): void { + this.cmpApi = null; + this.listenerId = undefined; + } + + /** + * Helper method to get base removal parameters + * Can be used by subclasses that need to remove event listeners + */ + protected getRemoveListenerParams(): Record | null { + const cmpApi = this.getCmpApi(); + const listenerId = this.getCmpListenerId(); + + // Comprehensive validation for all possible failure scenarios + if (cmpApi && typeof cmpApi === 'function' && listenerId !== undefined && listenerId !== null) { + return { + command: "removeEventListener", + callback: () => this.resetCmpApis(), + parameter: listenerId + }; + } + return null; + } + + /** + * Abstract method - each subclass implements its own removal logic + */ + abstract removeCmpEventListener(): void; +} + +/** + * TCF-specific CMP event manager + */ +export class TcfCmpEventManager extends BaseCmpEventManager { + private getConsentData: () => any; + + constructor(getConsentData?: () => any) { + super(); + this.getConsentData = getConsentData || (() => null); + } + + removeCmpEventListener(): void { + const params = this.getRemoveListenerParams(); + if (params) { + const consentData = this.getConsentData(); + params.apiVersion = consentData?.apiVersion || 2; + logInfo('Removing TCF CMP event listener'); + this.getCmpApi()(params); + } + } +} + +/** + * GPP-specific CMP event manager + * GPP doesn't require event listener removal, so this is empty + */ +export class GppCmpEventManager extends BaseCmpEventManager { + removeCmpEventListener(): void { + const params = this.getRemoveListenerParams(); + if (params) { + logInfo('Removing GPP CMP event listener'); + this.getCmpApi()(params); + } + } +} + +/** + * Factory function to create appropriate CMP event manager + */ +export function createCmpEventManager(type: 'tcf' | 'gpp', getConsentData?: () => any): CmpEventManager { + switch (type) { + case 'tcf': + return new TcfCmpEventManager(getConsentData); + case 'gpp': + return new GppCmpEventManager(); + default: + logError(`Unknown CMP type: ${type}`); + return null; + } +} diff --git a/libraries/consentManagement/cmUtils.ts b/libraries/consentManagement/cmUtils.ts index 88dfffef9cd..d8012241fc7 100644 --- a/libraries/consentManagement/cmUtils.ts +++ b/libraries/consentManagement/cmUtils.ts @@ -112,6 +112,12 @@ export interface BaseCMConfig { * for the user to interact with the CMP. */ actionTimeout?: number; + /** + * Flag to enable or disable the consent management module. + * When set to false, the module will be reset and disabled. + * Defaults to true when not specified. + */ + enabled?: boolean; } export interface IABCMConfig { @@ -136,6 +142,7 @@ export function configParser( parseConsentData, getNullConsent, cmpHandlers, + cmpEventCleanup, DEFAULT_CMP = 'iab', DEFAULT_CONSENT_TIMEOUT = 10000 } = {} as any @@ -167,6 +174,19 @@ export function configParser( getHook('requestBids').getHooks({hook: requestBidsHook}).remove(); buildActivityParams.getHooks({hook: attachActivityParams}).remove(); requestBidsHook = null; + logInfo(`${displayName} consentManagement module has been deactivated...`); + } + } + + function resetConsentDataHandler() { + reset(); + // Call module-specific CMP event cleanup if provided + if (typeof cmpEventCleanup === 'function') { + try { + cmpEventCleanup(); + } catch (e) { + logError(`Error during CMP event cleanup for ${displayName}:`, e); + } } } @@ -177,6 +197,14 @@ export function configParser( reset(); return {}; } + + // Check if module is explicitly disabled + if (cmConfig?.enabled === false) { + logWarn(msg(`config enabled is set to false, disabling consent manager module`)); + resetConsentDataHandler(); + return {}; + } + let cmpHandler; if (isStr(cmConfig.cmpApi)) { cmpHandler = cmConfig.cmpApi; diff --git a/libraries/dnt/index.js b/libraries/dnt/index.js index 221a20ee76c..1b03e848ca5 100644 --- a/libraries/dnt/index.js +++ b/libraries/dnt/index.js @@ -1,52 +1,11 @@ -function isDoNotTrackActive(value) { - if (value == null) { - return false; - } - - if (typeof value === 'string') { - const normalizedValue = value.toLowerCase(); - return normalizedValue === '1' || normalizedValue === 'yes'; - } - - return value === 1; -} - -function getTopWindow(win) { - try { - return win.top; - } catch (error) { - return win; - } +function _getDNT(win) { + return win.navigator.doNotTrack === '1' || win.doNotTrack === '1' || win.navigator.msDoNotTrack === '1' || win.navigator.doNotTrack?.toLowerCase?.() === 'yes'; } export function getDNT(win = window) { - const valuesToInspect = []; - - if (!win) { + try { + return _getDNT(win) || (win !== win.top && _getDNT(win.top)); + } catch (e) { return false; } - - const topWindow = getTopWindow(win); - - valuesToInspect.push(win.doNotTrack); - - if (topWindow && topWindow !== win) { - valuesToInspect.push(topWindow.doNotTrack); - } - - const navigatorInstances = new Set(); - - if (win.navigator) { - navigatorInstances.add(win.navigator); - } - - if (topWindow && topWindow.navigator) { - navigatorInstances.add(topWindow.navigator); - } - - navigatorInstances.forEach(navigatorInstance => { - valuesToInspect.push(navigatorInstance.doNotTrack, navigatorInstance.msDoNotTrack); - }); - - return valuesToInspect.some(isDoNotTrackActive); } diff --git a/libraries/extraWinDimensions/extraWinDimensions.js b/libraries/extraWinDimensions/extraWinDimensions.js deleted file mode 100644 index 5026a08b737..00000000000 --- a/libraries/extraWinDimensions/extraWinDimensions.js +++ /dev/null @@ -1,27 +0,0 @@ -import {canAccessWindowTop, internal as utilsInternals} from '../../src/utils.js'; -import {cachedGetter, internal as dimInternals} from '../../src/utils/winDimensions.js'; - -export const internal = { - fetchExtraDimensions -}; - -const extraDims = cachedGetter(() => internal.fetchExtraDimensions()); -/** - * Using these dimensions may flag you as a fingerprinting tool - * cfr. https://github.com/duckduckgo/tracker-radar/blob/main/build-data/generated/api_fingerprint_weights.json - */ -export const getExtraWinDimensions = extraDims.get; -dimInternals.resetters.push(extraDims.reset); - -function fetchExtraDimensions() { - const win = canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf(); - return { - outerWidth: win.outerWidth, - outerHeight: win.outerHeight, - screen: { - availWidth: win.screen?.availWidth, - availHeight: win.screen?.availHeight, - colorDepth: win.screen?.colorDepth, - } - }; -} diff --git a/libraries/fpdUtils/pageInfo.js b/libraries/fpdUtils/pageInfo.js index 8e02134e070..3d23f317085 100644 --- a/libraries/fpdUtils/pageInfo.js +++ b/libraries/fpdUtils/pageInfo.js @@ -69,3 +69,12 @@ export function getReferrer(bidRequest = {}, bidderRequest = {}) { } return pageUrl; } + +/** + * get the document complexity + * @param document + * @returns {*|number} + */ +export function getDomComplexity(document) { + return document?.querySelectorAll('*')?.length ?? -1; +} diff --git a/libraries/intentIqConstants/intentIqConstants.js b/libraries/intentIqConstants/intentIqConstants.js index 945bcfad4ad..0992f4c26b3 100644 --- a/libraries/intentIqConstants/intentIqConstants.js +++ b/libraries/intentIqConstants/intentIqConstants.js @@ -9,21 +9,12 @@ export const BLACK_LIST = 'L'; export const CLIENT_HINTS_KEY = '_iiq_ch'; export const EMPTY = 'EMPTY'; export const GVLID = '1323'; -export const VERSION = 0.31; +export const VERSION = 0.32; export const PREBID = 'pbjs'; export const HOURS_24 = 86400000; export const INVALID_ID = 'INVALID_ID'; -export const SCREEN_PARAMS = { - 0: 'windowInnerHeight', - 1: 'windowInnerWidth', - 2: 'devicePixelRatio', - 3: 'windowScreenHeight', - 4: 'windowScreenWidth', - 5: 'language' -}; - export const SYNC_REFRESH_MILL = 3600000; export const META_DATA_CONSTANT = 256; diff --git a/libraries/intentIqUtils/gamPredictionReport.js b/libraries/intentIqUtils/gamPredictionReport.js new file mode 100644 index 00000000000..09db065f494 --- /dev/null +++ b/libraries/intentIqUtils/gamPredictionReport.js @@ -0,0 +1,97 @@ +import { getEvents } from '../../src/events.js'; +import { logError } from '../../src/utils.js'; + +export function gamPredictionReport (gamObjectReference, sendData) { + try { + if (!gamObjectReference || !sendData) logError('Failed to get gamPredictionReport, required data is missed'); + const getSlotTargeting = (slot) => { + const kvs = {}; + try { + (slot.getTargetingKeys() || []).forEach((k) => { + kvs[k] = slot.getTargeting(k); + }); + } catch (e) { + logError('Failed to get targeting keys: ' + e); + } + return kvs; + }; + + const extractWinData = (gamEvent) => { + const slot = gamEvent.slot; + const targeting = getSlotTargeting(slot); + + const dataToSend = { + placementId: slot.getSlotElementId && slot.getSlotElementId(), + adUnitPath: slot.getAdUnitPath && slot.getAdUnitPath(), + bidderCode: targeting.hb_bidder ? targeting.hb_bidder[0] : null, + biddingPlatformId: 5 + }; + + if (dataToSend.placementId) { + // TODO check auto subscription to prebid events + const bidWonEvents = getEvents().filter((ev) => ev.eventType === 'bidWon'); + if (bidWonEvents.length) { + for (let i = bidWonEvents.length - 1; i >= 0; i--) { + const element = bidWonEvents[i]; + if ( + dataToSend.placementId === element.id && + targeting.hb_adid && + targeting.hb_adid[0] === element.args.adId + ) { + return; // don't send report if there was bidWon event earlier + } + } + } + const endEvents = getEvents().filter((ev) => ev.eventType === 'auctionEnd'); + if (endEvents.length) { + for (let i = endEvents.length - 1; i >= 0; i--) { + // starting from the last event + const element = endEvents[i]; + if (element.args?.adUnitCodes?.includes(dataToSend.placementId)) { + const defineRelevantData = (bid) => { + dataToSend.cpm = bid.cpm + 0.01; // add one cent to the cpm + dataToSend.currency = bid.currency; + dataToSend.originalCpm = bid.originalCpm; + dataToSend.originalCurrency = bid.originalCurrency; + dataToSend.status = bid.status; + dataToSend.prebidAuctionId = element.args?.auctionId; + }; + if (dataToSend.bidderCode) { + const relevantBid = element.args?.bidsReceived.find( + (item) => + item.bidder === dataToSend.bidderCode && + item.adUnitCode === dataToSend.placementId + ); + if (relevantBid) { + defineRelevantData(relevantBid); + break; + } + } else { + let highestBid = 0; + element.args?.bidsReceived.forEach((bid) => { + if (bid.adUnitCode === dataToSend.placementId && bid.cpm > highestBid) { + highestBid = bid.cpm; + defineRelevantData(bid); + } + }); + break; + } + } + } + } + } + return dataToSend; + }; + gamObjectReference.cmd.push(() => { + gamObjectReference.pubads().addEventListener('slotRenderEnded', (event) => { + if (event.isEmpty) return; + const data = extractWinData(event); + if (data?.cpm) { + sendData(data); + } + }); + }); + } catch (error) { + this.logger.error('Failed to subscribe to GAM: ' + error); + } +}; diff --git a/libraries/intentIqUtils/intentIqConfig.js b/libraries/intentIqUtils/intentIqConfig.js index 85c9111970b..3f2572f14fa 100644 --- a/libraries/intentIqUtils/intentIqConfig.js +++ b/libraries/intentIqUtils/intentIqConfig.js @@ -1,3 +1,3 @@ export const iiqServerAddress = (configParams, gdprDetected) => typeof configParams?.iiqServerAddress === 'string' ? configParams.iiqServerAddress : gdprDetected ? 'https://api-gdpr.intentiq.com' : 'https://api.intentiq.com' export const iiqPixelServerAddress = (configParams, gdprDetected) => typeof configParams?.iiqPixelServerAddress === 'string' ? configParams.iiqPixelServerAddress : gdprDetected ? 'https://sync-gdpr.intentiq.com' : 'https://sync.intentiq.com' -export const reportingServerAddress = (configParams, gdprDetected) => typeof configParams?.params?.reportingServerAddress === 'string' ? configParams.params.reportingServerAddress : gdprDetected ? 'https://reports-gdpr.intentiq.com/report' : 'https://reports.intentiq.com/report' +export const reportingServerAddress = (reportEndpoint, gdprDetected) => reportEndpoint && typeof reportEndpoint === 'string' ? reportEndpoint : gdprDetected ? 'https://reports-gdpr.intentiq.com/report' : 'https://reports.intentiq.com/report' diff --git a/libraries/medianetUtils/logKeys.js b/libraries/medianetUtils/logKeys.js index ced544d383f..94ddcb82abb 100644 --- a/libraries/medianetUtils/logKeys.js +++ b/libraries/medianetUtils/logKeys.js @@ -125,6 +125,7 @@ export const KeysMap = { 'floorData.floorRule as flrrule', 'floorRuleValue as flrRulePrice', 'serverLatencyMillis as rtime', + 'pbsExt', 'creativeId as pcrid', 'dbf', 'latestTargetedAuctionId as lacid', diff --git a/libraries/navigatorData/dnt.js b/libraries/navigatorData/dnt.js deleted file mode 100644 index 3101c0bc61c..00000000000 --- a/libraries/navigatorData/dnt.js +++ /dev/null @@ -1,5 +0,0 @@ -import { getDNT as baseGetDNT } from '../dnt/index.js'; - -export function getDNT(...args) { - return baseGetDNT(...args); -} diff --git a/libraries/navigatorData/navigatorData.js b/libraries/navigatorData/navigatorData.js index b957b4a1247..f1a34fc51eb 100644 --- a/libraries/navigatorData/navigatorData.js +++ b/libraries/navigatorData/navigatorData.js @@ -27,5 +27,3 @@ export function getDM(win = window) { } return dm; } - -export { getDNT } from './dnt.js'; diff --git a/libraries/nexx360Utils/index.js b/libraries/nexx360Utils/index.js deleted file mode 100644 index b7423148204..00000000000 --- a/libraries/nexx360Utils/index.js +++ /dev/null @@ -1,155 +0,0 @@ -import { deepAccess, deepSetValue, logInfo } from '../../src/utils.js'; -import {Renderer} from '../../src/Renderer.js'; -import { getCurrencyFromBidderRequest } from '../ortb2Utils/currency.js'; -import { INSTREAM, OUTSTREAM } from '../../src/video.js'; -import { BANNER, NATIVE } from '../../src/mediaTypes.js'; - -const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; - -/** - * Register the user sync pixels which should be dropped after the auction. - * - /** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse - * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions - * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync - * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests - * - */ - -/** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ -export function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (typeof serverResponses === 'object' && - serverResponses != null && - serverResponses.length > 0 && - serverResponses[0].hasOwnProperty('body') && - serverResponses[0].body.hasOwnProperty('ext') && - serverResponses[0].body.ext.hasOwnProperty('cookies') && - typeof serverResponses[0].body.ext.cookies === 'object') { - return serverResponses[0].body.ext.cookies.slice(0, 5); - } else { - return []; - } -}; - -function outstreamRender(response) { - response.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ - sizes: [response.width, response.height], - targetId: response.divId, - adResponse: response.vastXml, - rendererOptions: { - showBigPlayButton: false, - showProgressBar: 'bar', - showVolume: false, - allowFullscreen: true, - skippable: false, - content: response.vastXml - } - }); - }); -}; - -export function createRenderer(bid, url) { - const renderer = Renderer.install({ - id: bid.id, - url: url, - loaded: false, - adUnitCode: bid.ext.adUnitCode, - targetId: bid.ext.divId, - }); - renderer.setRender(outstreamRender); - return renderer; -}; - -export function enrichImp(imp, bidRequest) { - deepSetValue(imp, 'tagid', bidRequest.adUnitCode); - deepSetValue(imp, 'ext.adUnitCode', bidRequest.adUnitCode); - const divId = bidRequest.params.divId || bidRequest.adUnitCode; - deepSetValue(imp, 'ext.divId', divId); - if (imp.video) { - const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); - const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); - deepSetValue(imp, 'video.ext.playerSize', playerSize); - deepSetValue(imp, 'video.ext.context', videoContext); - } - return imp; -} - -export function enrichRequest(request, amxId, bidderRequest, pageViewId, bidderVersion) { - if (amxId) { - deepSetValue(request, 'ext.localStorage.amxId', amxId); - if (!request.user) request.user = {}; - if (!request.user.ext) request.user.ext = {}; - if (!request.user.ext.eids) request.user.ext.eids = []; - request.user.ext.eids.push({ - source: 'amxdt.net', - uids: [{ - id: `${amxId}`, - atype: 1 - }] - }); - } - deepSetValue(request, 'ext.version', '$prebid.version$'); - deepSetValue(request, 'ext.source', 'prebid.js'); - deepSetValue(request, 'ext.pageViewId', pageViewId); - deepSetValue(request, 'ext.bidderVersion', bidderVersion); - deepSetValue(request, 'cur', [getCurrencyFromBidderRequest(bidderRequest) || 'USD']); - if (!request.user) request.user = {}; - return request; -}; - -export function createResponse(bid, respBody) { - const response = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - creativeId: bid.crid, - currency: respBody.cur, - netRevenue: true, - ttl: 120, - mediaType: [OUTSTREAM, INSTREAM].includes(bid.ext.mediaType) ? 'video' : bid.ext.mediaType, - meta: { - advertiserDomains: bid.adomain, - demandSource: bid.ext.ssp, - }, - }; - if (bid.dealid) response.dealid = bid.dealid; - - if (bid.ext.mediaType === BANNER) response.ad = bid.adm; - if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.adm; - - if (bid.ext.mediaType === OUTSTREAM) { - response.renderer = createRenderer(bid, OUTSTREAM_RENDERER_URL); - if (bid.ext.divId) response.divId = bid.ext.divId - }; - - if (bid.ext.mediaType === NATIVE) { - try { - response.native = { ortb: JSON.parse(bid.adm) } - } catch (e) {} - } - return response; -} - -/** - * Get the AMX ID - * @return { string | false } false if localstorageNotEnabled - */ -export function getAmxId(storage, bidderCode) { - if (!storage.localStorageIsEnabled()) { - logInfo(`localstorage not enabled for ${bidderCode}`); - return false; - } - const amxId = storage.getDataFromLocalStorage('__amuidpb'); - return amxId || false; -} diff --git a/libraries/nexx360Utils/index.ts b/libraries/nexx360Utils/index.ts new file mode 100644 index 00000000000..0b8ed3fd719 --- /dev/null +++ b/libraries/nexx360Utils/index.ts @@ -0,0 +1,246 @@ +import { deepAccess, deepSetValue, generateUUID, logInfo } from '../../src/utils.js'; +import {Renderer} from '../../src/Renderer.js'; +import { getCurrencyFromBidderRequest } from '../ortb2Utils/currency.js'; +import { INSTREAM, OUTSTREAM } from '../../src/video.js'; +import { BANNER, MediaType, NATIVE, VIDEO } from '../../src/mediaTypes.js'; +import { BidResponse, VideoBidResponse } from '../../src/bidfactory.js'; +import { StorageManager } from '../../src/storageManager.js'; +import { BidRequest, ORTBImp, ORTBRequest, ORTBResponse } from '../../src/prebid.public.js'; +import { AdapterResponse, ServerResponse } from '../../src/adapters/bidderFactory.js'; + +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; + +let sessionId:string | null = null; + +const getSessionId = ():string => { + if (!sessionId) { + sessionId = generateUUID(); + } + return sessionId; +} + +let lastPageUrl:string = ''; +let requestCounter:number = 0; + +const getRequestCount = ():number => { + if (lastPageUrl === window.location.pathname) { + return ++requestCounter; + } + lastPageUrl = window.location.pathname; + return 0; +} + +export const getLocalStorageFunctionGenerator = < + T extends Record +>( + storage: StorageManager, + bidderCode: string, + storageKey: string, + jsonKey: keyof T + ): (() => T | null) => { + return () => { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for ${bidderCode}`); + return null; + } + + const output = storage.getDataFromLocalStorage(storageKey); + if (output === null) { + const storageElement: T = { [jsonKey]: generateUUID() } as T; + storage.setDataInLocalStorage(storageKey, JSON.stringify(storageElement)); + return storageElement; + } + try { + return JSON.parse(output) as T; + } catch (e) { + logInfo(`failed to parse localstorage for ${bidderCode}:`, e); + return null; + } + }; +}; + +export function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (typeof serverResponses === 'object' && + serverResponses != null && + serverResponses.length > 0 && + serverResponses[0].hasOwnProperty('body') && + serverResponses[0].body.hasOwnProperty('ext') && + serverResponses[0].body.ext.hasOwnProperty('cookies') && + typeof serverResponses[0].body.ext.cookies === 'object') { + return serverResponses[0].body.ext.cookies.slice(0, 5); + } else { + return []; + } +}; + +const createOustreamRendererFunction = ( + divId: string, + width: number, + height: number +) => (bidResponse: VideoBidResponse) => { + bidResponse.renderer.push(() => { + (window as any).ANOutstreamVideo.renderAd({ + sizes: [width, height], + targetId: divId, + adResponse: bidResponse.vastXml, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: bidResponse.vastXml + } + }); + }); +}; + +export type CreateRenderPayload = { + requestId: string, + vastXml: string, + divId: string, + width: number, + height: number +} + +export const createRenderer = ( + { requestId, vastXml, divId, width, height }: CreateRenderPayload +): Renderer | undefined => { + if (!vastXml) { + logInfo('No VAST in bidResponse'); + return; + } + const installPayload = { + id: requestId, + url: OUTSTREAM_RENDERER_URL, + loaded: false, + adUnitCode: divId, + targetId: divId, + }; + const renderer = Renderer.install(installPayload); + renderer.setRender(createOustreamRendererFunction(divId, width, height)); + return renderer; +}; + +export const enrichImp = (imp:ORTBImp, bidRequest:BidRequest): ORTBImp => { + deepSetValue(imp, 'tagid', bidRequest.adUnitCode); + deepSetValue(imp, 'ext.adUnitCode', bidRequest.adUnitCode); + const divId = bidRequest.params.divId || bidRequest.adUnitCode; + deepSetValue(imp, 'ext.divId', divId); + if (imp.video) { + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + deepSetValue(imp, 'video.ext.playerSize', playerSize); + deepSetValue(imp, 'video.ext.context', videoContext); + } + return imp; +} + +export const enrichRequest = ( + request: ORTBRequest, + amxId: string | null, + pageViewId: string, + bidderVersion: string):ORTBRequest => { + if (amxId) { + deepSetValue(request, 'ext.localStorage.amxId', amxId); + if (!request.user) request.user = {}; + if (!request.user.ext) request.user.ext = {}; + if (!request.user.ext.eids) request.user.ext.eids = []; + (request.user.ext.eids as any).push({ + source: 'amxdt.net', + uids: [{ + id: `${amxId}`, + atype: 1 + }] + }); + } + deepSetValue(request, 'ext.version', '$prebid.version$'); + deepSetValue(request, 'ext.source', 'prebid.js'); + deepSetValue(request, 'ext.pageViewId', pageViewId); + deepSetValue(request, 'ext.bidderVersion', bidderVersion); + deepSetValue(request, 'ext.sessionId', getSessionId()); + deepSetValue(request, 'ext.requestCounter', getRequestCount()); + deepSetValue(request, 'cur', [getCurrencyFromBidderRequest(request) || 'USD']); + if (!request.user) request.user = {}; + return request; +}; + +export function createResponse(bid:any, ortbResponse:any): BidResponse { + let mediaType: MediaType = BANNER; + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType as string)) mediaType = VIDEO; + if (bid.ext.mediaType === NATIVE) mediaType = NATIVE; + const response:any = { + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + currency: ortbResponse.cur, + netRevenue: true, + ttl: 120, + mediaType, + meta: { + advertiserDomains: bid.adomain, + demandSource: bid.ext.ssp, + }, + }; + if (bid.dealid) response.dealid = bid.dealid; + + if (bid.ext.mediaType === BANNER) response.ad = bid.adm; + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType as string)) response.vastXml = bid.adm; + if (bid.ext.mediaType === OUTSTREAM && (bid.ext.divId || bid.ext.adUnitCode)) { + const renderer = createRenderer({ + requestId: response.requestId, + vastXml: response.vastXml, + divId: bid.ext.divId || bid.ext.adUnitCode, + width: response.width, + height: response.height + }); + if (renderer) { + response.renderer = renderer; + response.divId = bid.ext.divId; + } else { + logInfo('Could not create renderer for outstream bid'); + } + }; + + if (bid.ext.mediaType === NATIVE) { + try { + response.native = { ortb: JSON.parse(bid.adm) } + } catch (e) {} + } + return response as BidResponse; +} + +export const interpretResponse = (serverResponse: ServerResponse): AdapterResponse => { + if (!serverResponse.body) return []; + const respBody = serverResponse.body as ORTBResponse; + if (!respBody.seatbid || respBody.seatbid.length === 0) return []; + + const responses: BidResponse[] = []; + for (let i = 0; i < respBody.seatbid.length; i++) { + const seatbid = respBody.seatbid[i]; + for (let j = 0; j < seatbid.bid.length; j++) { + const bid = seatbid.bid[j]; + const response:BidResponse = createResponse(bid, respBody); + responses.push(response); + } + } + return responses; +} + +/** + * Get the AMX ID + * @return { string | false } false if localstorageNotEnabled + */ +export const getAmxId = ( + storage: StorageManager, + bidderCode: string +): string | null => { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for ${bidderCode}`); + return null; + } + const amxId = storage.getDataFromLocalStorage('__amuidpb'); + return amxId || null; +} diff --git a/libraries/objectGuard/objectGuard.js b/libraries/objectGuard/objectGuard.js index 784c3f1444d..f8d895be7cb 100644 --- a/libraries/objectGuard/objectGuard.js +++ b/libraries/objectGuard/objectGuard.js @@ -1,5 +1,5 @@ -import {isData, objectTransformer, sessionedApplies} from '../../src/activities/redactor.js'; -import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js'; +import {isData, sessionedApplies} from '../../src/activities/redactor.js'; +import {deepEqual, logWarn} from '../../src/utils.js'; /** * @typedef {import('../src/activities/redactor.js').TransformationRuleDef} TransformationRuleDef @@ -12,66 +12,191 @@ import {deepAccess, deepClone, deepEqual, deepSetValue} from '../../src/utils.js /** * Create a factory function for object guards using the given rules. * - * An object guard is a pair {obj, verify} where: - * - `obj` is a view on the guarded object that applies "redact" rules (the same rules used in activites/redactor.js) - * - `verify` is a function that, when called, will check that the guarded object was not modified - * in a way that violates any "write protect" rules, and rolls back any offending changes. + * An object guard is a view on the guarded object that applies "redact" rules (the same rules used in activites/redactor.js), + * and prevents writes (including deltes) that violate "write protect" rules. * * This is meant to provide sandboxed version of a privacy-sensitive object, where reads * are filtered through redaction rules and writes are checked against write protect rules. * - * @param {Array[TransformationRule]} rules - * @return {function(*, ...[*]): ObjectGuard} */ export function objectGuard(rules) { const root = {}; - const writeRules = []; + + // rules are associated with specific portions of the object, e.g. "user.eids" + // build a tree representation of them, where the root is the object itself, + // and each node's children are properties of the corresponding (nested) object. rules.forEach(rule => { - if (rule.wp) writeRules.push(rule); - if (!rule.get) return; rule.paths.forEach(path => { let node = root; path.split('.').forEach(el => { - node.children = node.children || {}; - node.children[el] = node.children[el] || {}; + node.children = node.children ?? {}; + node.children[el] = node.children[el] ?? { parent: node, path: node.path ? `${node.path}.${el}` : el }; node = node.children[el]; - }) - node.rule = rule; + node.wpRules = node.wpRules ?? []; + node.redactRules = node.redactRules ?? []; + }); + (rule.wp ? node.wpRules : node.redactRules).push(rule); + if (rule.wp) { + // mark the whole path as write protected, so that write operations + // on parents do not need to walk down the tree + let parent = node; + while (parent && !parent.hasWP) { + parent.hasWP = true; + parent = parent.parent; + } + } }); }); - const wpTransformer = objectTransformer(writeRules); + function getRedactRule(node) { + if (node.redactRule == null) { + node.redactRule = node.redactRules.length === 0 ? false : { + check: (applies) => node.redactRules.some(applies), + get(val) { + for (const rule of node.redactRules) { + val = rule.get(val); + if (!isData(val)) break; + } + return val; + } + } + } + return node.redactRule; + } + + function getWPRule(node) { + if (node.wpRule == null) { + node.wpRule = node.wpRules.length === 0 ? false : { + check: (applies) => node.wpRules.some(applies), + } + } + return node.wpRule; + } + + /** + * clean up `newValue` so that it doesn't violate any write protect rules + * when set onto the property represented by 'node'. + * + * This is done substituting (portions of) `curValue` when some rule is violated. + */ + function cleanup(node, curValue, newValue, applies) { + if ( + !node.hasWP || + (!isData(curValue) && !isData(newValue)) || + deepEqual(curValue, newValue) + ) { + return newValue; + } + const rule = getWPRule(node); + if (rule && rule.check(applies)) { + return curValue; + } + if (node.children) { + for (const [prop, child] of Object.entries(node.children)) { + const propValue = cleanup(child, curValue?.[prop], newValue?.[prop], applies); + if (newValue != null && typeof newValue === 'object') { + if (!isData(propValue) && !curValue?.hasOwnProperty(prop)) { + delete newValue[prop]; + } else { + newValue[prop] = propValue; + } + } else { + logWarn(`Invalid value set for '${node.path}', expected an object`, newValue); + return curValue; + } + } + } + return newValue; + } + + function isDeleteAllowed(node, curValue, applies) { + if (!node.hasWP || !isData(curValue)) { + return true; + } + const rule = getWPRule(node); + if (rule && rule.check(applies)) { + return false; + } + if (node.children) { + for (const [prop, child] of Object.entries(node.children)) { + if (!isDeleteAllowed(child, curValue?.[prop], applies)) { + return false; + } + } + } + return true; + } + + function mkGuard(obj, tree, final, applies, cache = new WeakMap()) { + // If this object is already proxied, return the cached proxy + if (cache.has(obj)) { + return cache.get(obj); + } - function mkGuard(obj, tree, applies) { - return new Proxy(obj, { + const proxy = new Proxy(obj, { get(target, prop, receiver) { const val = Reflect.get(target, prop, receiver); - if (tree.hasOwnProperty(prop)) { - const {children, rule} = tree[prop]; - if (children && val != null && typeof val === 'object') { - return mkGuard(val, children, applies); - } else if (rule && isData(val) && applies(rule)) { - return rule.get(val); + if (final && val != null && typeof val === 'object') { + // a parent property has write protect rules, keep guarding + return mkGuard(val, tree, final, applies, cache) + } else if (tree.children?.hasOwnProperty(prop)) { + const {children, hasWP} = tree.children[prop]; + if ((children || hasWP) && val != null && typeof val === 'object') { + // some nested properties have rules, return a guard for the branch + return mkGuard(val, tree.children?.[prop] || tree, final || children == null, applies, cache); + } else if (isData(val)) { + // if this property has redact rules, apply them + const rule = getRedactRule(tree.children[prop]); + if (rule && rule.check(applies)) { + return rule.get(val); + } } } return val; }, + set(target, prop, newValue, receiver) { + if (final) { + // a parent property has rules, apply them + const rule = getWPRule(tree); + if (rule && rule.check(applies)) { + return true; + } + } + if (tree.children?.hasOwnProperty(prop)) { + // apply all (possibly nested) write protect rules + const curValue = Reflect.get(target, prop, receiver); + newValue = cleanup(tree.children[prop], curValue, newValue, applies); + if (!isData(newValue) && !target.hasOwnProperty(prop)) { + return true; + } + } + return Reflect.set(target, prop, newValue, receiver); + }, + deleteProperty(target, prop) { + if (final) { + // a parent property has rules, apply them + const rule = getWPRule(tree); + if (rule && rule.check(applies)) { + return true; + } + } + if (tree.children?.hasOwnProperty(prop) && !isDeleteAllowed(tree.children[prop], target[prop], applies)) { + // some nested properties should not be deleted + return true; + } + return Reflect.deleteProperty(target, prop); + } }); - } - function mkVerify(transformResult) { - return function () { - transformResult.forEach(fn => fn()); - } + // Cache the proxy before returning + cache.set(obj, proxy); + return proxy; } return function guard(obj, ...args) { const session = {}; - return { - obj: mkGuard(obj, root.children || {}, sessionedApplies(session, ...args)), - verify: mkVerify(wpTransformer(session, obj, ...args)) - } + return mkGuard(obj, root, false, sessionedApplies(session, ...args)) }; } @@ -82,20 +207,5 @@ export function objectGuard(rules) { export function writeProtectRule(ruleDef) { return Object.assign({ wp: true, - run(root, path, object, property, applies) { - const origHasProp = object && object.hasOwnProperty(property); - const original = origHasProp ? object[property] : undefined; - const origCopy = origHasProp && original != null && typeof original === 'object' ? deepClone(original) : original; - return function () { - const object = path == null ? root : deepAccess(root, path); - const finalHasProp = object && isData(object[property]); - const finalValue = finalHasProp ? object[property] : undefined; - if (!origHasProp && finalHasProp && applies()) { - delete object[property]; - } else if ((origHasProp !== finalHasProp || finalValue !== original || !deepEqual(finalValue, origCopy)) && applies()) { - deepSetValue(root, (path == null ? [] : [path]).concat(property).join('.'), origCopy); - } - } - } }, ruleDef) } diff --git a/libraries/objectGuard/ortbGuard.js b/libraries/objectGuard/ortbGuard.js index 62918d55548..324c7976ab1 100644 --- a/libraries/objectGuard/ortbGuard.js +++ b/libraries/objectGuard/ortbGuard.js @@ -7,11 +7,7 @@ import { ORTB_UFPD_PATHS } from '../../src/activities/redactor.js'; import {objectGuard, writeProtectRule} from './objectGuard.js'; -import {mergeDeep} from '../../src/utils.js'; - -/** - * @typedef {import('./objectGuard.js').ObjectGuard} ObjectGuard - */ +import {logError} from '../../src/utils.js'; function ortb2EnrichRules(isAllowed = isActivityAllowed) { return [ @@ -32,20 +28,10 @@ export function ortb2GuardFactory(isAllowed = isActivityAllowed) { return objectGuard(ortb2TransmitRules(isAllowed).concat(ortb2EnrichRules(isAllowed))); } -/** - * - * - * @typedef {Function} ortb2Guard - * @param {{}} ortb2 ORTB object to guard - * @param {{}} params activity params to use for activity checks - * @returns {ObjectGuard} - */ - /* * Get a guard for an ORTB object. Read access is restricted in the same way it'd be redacted (see activites/redactor.js); * and writes are checked against the enrich* activites. * - * @type ortb2Guard */ export const ortb2Guard = ortb2GuardFactory(); @@ -53,40 +39,44 @@ export function ortb2FragmentsGuardFactory(guardOrtb2 = ortb2Guard) { return function guardOrtb2Fragments(fragments, params) { fragments.global = fragments.global || {}; fragments.bidder = fragments.bidder || {}; - const bidders = new Set(Object.keys(fragments.bidder)); - const verifiers = []; - - function makeGuard(ortb2) { - const guard = guardOrtb2(ortb2, params); - verifiers.push(guard.verify); - return guard.obj; - } - - const obj = { - global: makeGuard(fragments.global), - bidder: Object.fromEntries(Object.entries(fragments.bidder).map(([bidder, ortb2]) => [bidder, makeGuard(ortb2)])) + const guard = { + global: guardOrtb2(fragments.global, params), + bidder: new Proxy(fragments.bidder, { + get(target, prop, receiver) { + let bidderData = Reflect.get(target, prop, receiver); + if (bidderData != null) { + bidderData = guardOrtb2(bidderData, params) + } + return bidderData; + }, + set(target, prop, newValue, receiver) { + if (newValue == null || typeof newValue !== 'object') { + logError(`ortb2Fragments.bidder[bidderCode] must be an object`); + } + let bidderData = Reflect.get(target, prop, receiver); + if (bidderData == null) { + bidderData = target[prop] = {}; + } + bidderData = guardOrtb2(bidderData, params); + Object.entries(newValue).forEach(([prop, value]) => { + bidderData[prop] = value; + }) + return true; + } + }) }; - return { - obj, - verify() { - Object.entries(obj.bidder) - .filter(([bidder]) => !bidders.has(bidder)) - .forEach(([bidder, ortb2]) => { - const repl = {}; - const guard = guardOrtb2(repl, params); - mergeDeep(guard.obj, ortb2); - guard.verify(); - fragments.bidder[bidder] = repl; - }) - verifiers.forEach(fn => fn()); - } - } + return Object.defineProperties( + {}, + Object.fromEntries( + // disallow overwriting of the top level `global` / `bidder` + Object.entries(guard).map(([prop, obj]) => [prop, {get: () => obj}]) + ) + ) } } /** * Get a guard for an ortb2Fragments object. - * @type {function(*, *): ObjectGuard} */ export const guardOrtb2Fragments = ortb2FragmentsGuardFactory(); diff --git a/libraries/pbsExtensions/processors/pageViewIds.js b/libraries/pbsExtensions/processors/pageViewIds.js new file mode 100644 index 00000000000..c71d32b7735 --- /dev/null +++ b/libraries/pbsExtensions/processors/pageViewIds.js @@ -0,0 +1,9 @@ +import {deepSetValue} from '../../../src/utils.js'; + +export function setRequestExtPrebidPageViewIds(ortbRequest, bidderRequest) { + deepSetValue( + ortbRequest, + `ext.prebid.page_view_ids.${bidderRequest.bidderCode}`, + bidderRequest.pageViewId + ); +} diff --git a/libraries/pbsExtensions/processors/pbs.js b/libraries/pbsExtensions/processors/pbs.js index 6d94a8727ff..3fa97ae674b 100644 --- a/libraries/pbsExtensions/processors/pbs.js +++ b/libraries/pbsExtensions/processors/pbs.js @@ -1,5 +1,5 @@ import {BID_RESPONSE, IMP, REQUEST, RESPONSE} from '../../../src/pbjsORTB.js'; -import {deepAccess, isPlainObject, isStr, mergeDeep} from '../../../src/utils.js'; +import {isPlainObject, isStr, mergeDeep} from '../../../src/utils.js'; import {extPrebidMediaType} from './mediaType.js'; import {setRequestExtPrebidAliases} from './aliases.js'; import {setImpBidParams} from './params.js'; @@ -7,6 +7,7 @@ import {setImpAdUnitCode} from './adUnitCode.js'; import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js'; import {setBidResponseVideoCache} from './video.js'; import {addEventTrackers} from './eventTrackers.js'; +import {setRequestExtPrebidPageViewIds} from './pageViewIds.js'; export const PBS_PROCESSORS = { [REQUEST]: { @@ -21,7 +22,11 @@ export const PBS_PROCESSORS = { extPrebidAliases: { // sets ext.prebid.aliases fn: setRequestExtPrebidAliases - } + }, + extPrebidPageViewIds: { + // sets ext.prebid.page_view_ids + fn: setRequestExtPrebidPageViewIds + }, }, [IMP]: { params: { @@ -82,20 +87,36 @@ export const PBS_PROCESSORS = { }, [RESPONSE]: { serverSideStats: { - // updates bidderRequest and bidRequests with serverErrors from ext.errors and serverResponseTimeMs from ext.responsetimemillis + // updates bidderRequest and bidRequests with fields from response.ext + // - bidder-scoped for 'errors' and 'responsetimemillis' + // - copy-as-is for all other fields fn(response, ortbResponse, context) { - Object.entries({ + const bidder = context.bidderRequest?.bidderCode; + const ext = ortbResponse?.ext; + if (!ext) return; + + const FIELD_MAP = { errors: 'serverErrors', responsetimemillis: 'serverResponseTimeMs' - }).forEach(([serverName, clientName]) => { - const value = deepAccess(ortbResponse, `ext.${serverName}.${context.bidderRequest.bidderCode}`); - if (value) { - context.bidderRequest[clientName] = value; - context.bidRequests.forEach(bid => { - bid[clientName] = value; - }); + }; + + Object.entries(ext).forEach(([field, extValue]) => { + if (FIELD_MAP[field]) { + // Skip mapped fields if no bidder + if (!bidder) return; + const value = extValue?.[bidder]; + if (value !== undefined) { + const clientName = FIELD_MAP[field]; + context.bidderRequest[clientName] = value; + context.bidRequests?.forEach(bid => { + bid[clientName] = value; + }); + } + } else if (extValue !== undefined) { + context.bidderRequest.pbsExt = context.bidderRequest.pbsExt || {}; + context.bidderRequest.pbsExt[field] = extValue; } - }) + }); } }, } diff --git a/libraries/riseUtils/index.js b/libraries/riseUtils/index.js index 21f2d72660f..77ca87f0fca 100644 --- a/libraries/riseUtils/index.js +++ b/libraries/riseUtils/index.js @@ -12,7 +12,7 @@ import { } from '../../src/utils.js'; import {BANNER, NATIVE, VIDEO} from '../../src/mediaTypes.js'; import {config} from '../../src/config.js'; -import { getDNT } from '../navigatorData/dnt.js'; +import { getDNT } from '../dnt/index.js'; import {ADAPTER_VERSION, DEFAULT_CURRENCY, DEFAULT_TTL, SUPPORTED_AD_TYPES} from './constants.js'; import {getGlobalVarName} from '../../src/buildOptions.js'; @@ -385,7 +385,7 @@ export function generateGeneralParams(generalObject, bidderRequest, adapterVersi tmax: timeout }; - const userIdsParam = getBidIdParameter('userId', generalObject); + const userIdsParam = getBidIdParameter('userIdAsEids', generalObject); if (userIdsParam) { generalParams.userIds = JSON.stringify(userIdsParam); } diff --git a/libraries/uid2IdSystemShared/uid2IdSystem_shared.js b/libraries/uid2IdSystemShared/uid2IdSystem_shared.js index 71a993e6534..2230fb4efb0 100644 --- a/libraries/uid2IdSystemShared/uid2IdSystem_shared.js +++ b/libraries/uid2IdSystemShared/uid2IdSystem_shared.js @@ -44,6 +44,7 @@ export class Uid2ApiClient { ResponseToRefreshResult(response) { if (this.isValidRefreshResponse(response)) { if (response.status === 'success') { return { status: response.status, identity: response.body }; } + if (response.status === 'optout') { return { status: response.status, identity: 'optout' }; } return response; } else { return prependMessage(`Response didn't contain a valid status`); } } diff --git a/libraries/userAgentUtils/constants.js b/libraries/userAgentUtils/constants.js new file mode 100644 index 00000000000..5bab9396956 --- /dev/null +++ b/libraries/userAgentUtils/constants.js @@ -0,0 +1,12 @@ +export const BOL_LIKE_USER_AGENTS = [ + 'Mediapartners-Google', + 'facebookexternalhit', + 'amazon-kendra', + 'crawler', + 'bot', + 'spider', + 'python', + 'curl', + 'wget', + 'httpclient' +]; diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js index 08432936858..fa2cea1b6fa 100644 --- a/libraries/vidazooUtils/bidderUtils.js +++ b/libraries/vidazooUtils/bidderUtils.js @@ -6,7 +6,8 @@ import { parseSizesInput, parseUrl, triggerPixel, - uniques + uniques, + getWinDimensions } from '../../src/utils.js'; import {chunk} from '../chunk/chunk.js'; import {CURRENCY, DEAL_ID_EXPIRY, SESSION_ID_KEY, TTL_SECONDS, UNIQUE_DEAL_ID_EXPIRY} from './constants.js'; @@ -280,7 +281,7 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder uniqueDealId: uniqueDealId, bidderVersion: bidderVersion, prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, + res: getScreenResolution(), schain: schain, mediaTypes: mediaTypes, isStorageAllowed: isStorageAllowed, @@ -374,6 +375,15 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder return data; } +function getScreenResolution() { + const dimensions = getWinDimensions(); + const width = dimensions?.screen?.width; + const height = dimensions?.screen?.height; + if (width != null && height != null) { + return `${width}x${height}` + } +} + export function createInterpretResponseFn(bidderCode, allowSingleRequest) { return function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { diff --git a/metadata/modules.json b/metadata/modules.json index 6805550f8db..a08bfd3b4ac 100644 --- a/metadata/modules.json +++ b/metadata/modules.json @@ -85,6 +85,13 @@ "gvlid": 617, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "adbro", + "aliasOf": null, + "gvlid": 1316, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "adbutler", @@ -484,6 +491,27 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "infinety", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "qohere", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "blutonic", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "admaru", @@ -652,6 +680,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "adocean", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "adot", @@ -911,6 +946,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "harrenmedia", + "aliasOf": "adverxo", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "adxcg", @@ -1667,6 +1709,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "clickio", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "codefuel", @@ -1891,6 +1940,13 @@ "gvlid": 541, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "defineMedia", + "aliasOf": null, + "gvlid": 440, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "deltaprojects", @@ -2080,6 +2136,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "empower", + "aliasOf": null, + "gvlid": 1248, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "emtv", @@ -2773,6 +2836,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "vaayaMedia", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "livewrapped", @@ -3081,6 +3151,13 @@ "gvlid": 898, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "msft", + "aliasOf": null, + "gvlid": 32, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "my6sense", @@ -3270,6 +3347,13 @@ "gvlid": 1485, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "ybidder", + "aliasOf": "nexx360", + "gvlid": 1253, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "nobid", @@ -3284,6 +3368,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "nuba", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "ogury", @@ -3295,7 +3386,7 @@ "componentType": "bidder", "componentName": "omnidex", "aliasOf": null, - "gvlid": null, + "gvlid": 1463, "disclosureURL": null }, { @@ -3512,7 +3603,7 @@ "componentType": "bidder", "componentName": "pgamssp", "aliasOf": null, - "gvlid": 1353, + "gvlid": null, "disclosureURL": null }, { @@ -3613,6 +3704,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "publicgood", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "publir", @@ -3942,6 +4040,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "scalibur", + "aliasOf": null, + "gvlid": 1471, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "scattered", @@ -4152,6 +4257,20 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "amcom", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "adastra", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "smartico", @@ -4411,6 +4530,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "topon", + "aliasOf": null, + "gvlid": 1305, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "tpmn", @@ -4509,6 +4635,13 @@ "gvlid": null, "disclosureURL": null }, + { + "componentType": "bidder", + "componentName": "uniquest_widget", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + }, { "componentType": "bidder", "componentName": "unruly", @@ -5157,7 +5290,7 @@ { "componentType": "rtd", "componentName": "permutive", - "gvlid": null, + "gvlid": 361, "disclosureURL": null }, { @@ -5562,7 +5695,7 @@ { "componentType": "userId", "componentName": "permutiveIdentityManagerId", - "gvlid": null, + "gvlid": 361, "disclosureURL": null, "aliasOf": null }, diff --git a/metadata/modules/33acrossBidAdapter.json b/metadata/modules/33acrossBidAdapter.json index e52cacb49dc..d532327e06e 100644 --- a/metadata/modules/33acrossBidAdapter.json +++ b/metadata/modules/33acrossBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://platform.33across.com/disclosures.json": { - "timestamp": "2025-10-01T19:33:04.700Z", + "timestamp": "2025-11-19T20:51:08.096Z", "disclosures": [] } }, diff --git a/metadata/modules/33acrossIdSystem.json b/metadata/modules/33acrossIdSystem.json index 26cfcccdb2e..50f8e7b1997 100644 --- a/metadata/modules/33acrossIdSystem.json +++ b/metadata/modules/33acrossIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://platform.33across.com/disclosures.json": { - "timestamp": "2025-10-01T19:33:04.810Z", + "timestamp": "2025-11-19T20:51:08.188Z", "disclosures": [] } }, diff --git a/metadata/modules/acuityadsBidAdapter.json b/metadata/modules/acuityadsBidAdapter.json index d82802dbf51..39f13157d79 100644 --- a/metadata/modules/acuityadsBidAdapter.json +++ b/metadata/modules/acuityadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.acuityads.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:04.814Z", + "timestamp": "2025-11-19T20:51:08.191Z", "disclosures": [] } }, diff --git a/metadata/modules/adagioBidAdapter.json b/metadata/modules/adagioBidAdapter.json index 4ce2b0c3e31..4ef5ccfc5ae 100644 --- a/metadata/modules/adagioBidAdapter.json +++ b/metadata/modules/adagioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adagio.io/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:04.852Z", + "timestamp": "2025-11-19T20:51:08.247Z", "disclosures": [] } }, diff --git a/metadata/modules/adagioRtdProvider.json b/metadata/modules/adagioRtdProvider.json index af04a77e91f..5d130b541fb 100644 --- a/metadata/modules/adagioRtdProvider.json +++ b/metadata/modules/adagioRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adagio.io/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:04.890Z", + "timestamp": "2025-11-19T20:51:08.295Z", "disclosures": [] } }, diff --git a/metadata/modules/adbroBidAdapter.json b/metadata/modules/adbroBidAdapter.json new file mode 100644 index 00000000000..ff4d88380d3 --- /dev/null +++ b/metadata/modules/adbroBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://tag.adbro.me/privacy/devicestorage.json": { + "timestamp": "2025-11-19T20:51:08.295Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "adbro", + "aliasOf": null, + "gvlid": 1316, + "disclosureURL": "https://tag.adbro.me/privacy/devicestorage.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/addefendBidAdapter.json b/metadata/modules/addefendBidAdapter.json index c4a77b8a6ca..5732298509a 100644 --- a/metadata/modules/addefendBidAdapter.json +++ b/metadata/modules/addefendBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.addefend.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:04.890Z", + "timestamp": "2025-11-19T20:51:08.584Z", "disclosures": [] } }, diff --git a/metadata/modules/adfBidAdapter.json b/metadata/modules/adfBidAdapter.json index 139bd91f8a5..e18260a2a5d 100644 --- a/metadata/modules/adfBidAdapter.json +++ b/metadata/modules/adfBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://site.adform.com/assets/devicestorage.json": { - "timestamp": "2025-10-01T19:33:05.783Z", + "timestamp": "2025-11-19T20:51:09.232Z", "disclosures": [] } }, diff --git a/metadata/modules/adfusionBidAdapter.json b/metadata/modules/adfusionBidAdapter.json index 5d9c71bc0d8..00a4a50710f 100644 --- a/metadata/modules/adfusionBidAdapter.json +++ b/metadata/modules/adfusionBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spicyrtb.com/static/iab-disclosure.json": { - "timestamp": "2025-10-01T19:33:05.783Z", + "timestamp": "2025-11-19T20:51:09.232Z", "disclosures": [] } }, diff --git a/metadata/modules/adheseBidAdapter.json b/metadata/modules/adheseBidAdapter.json index b3612c3ffff..8b35bf5d541 100644 --- a/metadata/modules/adheseBidAdapter.json +++ b/metadata/modules/adheseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adhese.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:06.148Z", + "timestamp": "2025-11-19T20:51:09.608Z", "disclosures": [] } }, diff --git a/metadata/modules/adipoloBidAdapter.json b/metadata/modules/adipoloBidAdapter.json index b794bf240cf..a106bd54e64 100644 --- a/metadata/modules/adipoloBidAdapter.json +++ b/metadata/modules/adipoloBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adipolo.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:06.413Z", + "timestamp": "2025-11-19T20:51:09.872Z", "disclosures": [] } }, diff --git a/metadata/modules/adkernelAdnBidAdapter.json b/metadata/modules/adkernelAdnBidAdapter.json index b276b321904..2e3a6eba0b5 100644 --- a/metadata/modules/adkernelAdnBidAdapter.json +++ b/metadata/modules/adkernelAdnBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.adkernel.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:06.541Z", + "timestamp": "2025-11-19T20:51:10.019Z", "disclosures": [ { "identifier": "adk_rtb_conv_id", diff --git a/metadata/modules/adkernelBidAdapter.json b/metadata/modules/adkernelBidAdapter.json index 53c9bb1a8f8..e073819755d 100644 --- a/metadata/modules/adkernelBidAdapter.json +++ b/metadata/modules/adkernelBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.adkernel.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:06.563Z", + "timestamp": "2025-11-19T20:51:10.063Z", "disclosures": [ { "identifier": "adk_rtb_conv_id", @@ -17,15 +17,15 @@ ] }, "https://data.converge-digital.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:06.563Z", + "timestamp": "2025-11-19T20:51:10.063Z", "disclosures": [] }, "https://spinx.biz/tcf-spinx.json": { - "timestamp": "2025-10-01T19:33:06.620Z", + "timestamp": "2025-11-19T20:51:10.114Z", "disclosures": [] }, "https://gdpr.memob.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:07.374Z", + "timestamp": "2025-11-19T20:51:10.823Z", "disclosures": [] } }, @@ -316,6 +316,27 @@ "aliasOf": "adkernel", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "infinety", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "qohere", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "blutonic", + "aliasOf": "adkernel", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/admaticBidAdapter.json b/metadata/modules/admaticBidAdapter.json index 2f6d6dfbeba..49230562aff 100644 --- a/metadata/modules/admaticBidAdapter.json +++ b/metadata/modules/admaticBidAdapter.json @@ -2,19 +2,24 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.admatic.de/iab-europe/tcfv2/disclosure.json": { - "timestamp": "2025-10-01T19:33:07.666Z", + "timestamp": "2025-11-19T20:51:11.408Z", "disclosures": [ { "identifier": "px_pbjs", "type": "web", - "maxAgeSeconds": null, "purposes": [] } ] }, "https://adtarget.com.tr/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:07.509Z", - "disclosures": [] + "timestamp": "2025-11-19T20:51:10.948Z", + "disclosures": [ + { + "identifier": "adt_pbjs", + "type": "web", + "purposes": [] + } + ] } }, "components": [ diff --git a/metadata/modules/admixerBidAdapter.json b/metadata/modules/admixerBidAdapter.json index 43f17b06899..37962a173e4 100644 --- a/metadata/modules/admixerBidAdapter.json +++ b/metadata/modules/admixerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admixer.com/tcf.json": { - "timestamp": "2025-10-01T19:33:07.667Z", + "timestamp": "2025-11-19T20:51:11.409Z", "disclosures": [] } }, diff --git a/metadata/modules/admixerIdSystem.json b/metadata/modules/admixerIdSystem.json index f7f4b05c99d..cedbe489a6d 100644 --- a/metadata/modules/admixerIdSystem.json +++ b/metadata/modules/admixerIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admixer.com/tcf.json": { - "timestamp": "2025-10-01T19:33:08.057Z", + "timestamp": "2025-11-19T20:51:11.787Z", "disclosures": [] } }, diff --git a/metadata/modules/adnowBidAdapter.json b/metadata/modules/adnowBidAdapter.json index 438e6812df2..21d4b63fd47 100644 --- a/metadata/modules/adnowBidAdapter.json +++ b/metadata/modules/adnowBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adnow.com/vdsod.json": { - "timestamp": "2025-10-01T19:33:08.057Z", + "timestamp": "2025-11-19T20:51:11.787Z", "disclosures": [ { "identifier": "SC_unique_*", diff --git a/metadata/modules/adnuntiusBidAdapter.json b/metadata/modules/adnuntiusBidAdapter.json index 41bb21bd3e5..8137782114a 100644 --- a/metadata/modules/adnuntiusBidAdapter.json +++ b/metadata/modules/adnuntiusBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.adnuntius.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:08.289Z", + "timestamp": "2025-11-19T20:51:12.039Z", "disclosures": [ { "identifier": "adn.metaData", diff --git a/metadata/modules/adnuntiusRtdProvider.json b/metadata/modules/adnuntiusRtdProvider.json index 5a1ce5bae2d..7cbe053afbd 100644 --- a/metadata/modules/adnuntiusRtdProvider.json +++ b/metadata/modules/adnuntiusRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.adnuntius.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:08.623Z", + "timestamp": "2025-11-19T20:51:12.355Z", "disclosures": [ { "identifier": "adn.metaData", diff --git a/metadata/modules/adoceanBidAdapter.json b/metadata/modules/adoceanBidAdapter.json new file mode 100644 index 00000000000..9743e913146 --- /dev/null +++ b/metadata/modules/adoceanBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "adocean", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/adotBidAdapter.json b/metadata/modules/adotBidAdapter.json index 4dca585fe4c..4583bae817b 100644 --- a/metadata/modules/adotBidAdapter.json +++ b/metadata/modules/adotBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.adotmob.com/tcf/tcf.json": { - "timestamp": "2025-10-01T19:33:08.624Z", + "timestamp": "2025-11-19T20:51:12.355Z", "disclosures": [] } }, diff --git a/metadata/modules/adponeBidAdapter.json b/metadata/modules/adponeBidAdapter.json index 387624f2ffb..7e729f814b7 100644 --- a/metadata/modules/adponeBidAdapter.json +++ b/metadata/modules/adponeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adserver.adpone.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:08.671Z", + "timestamp": "2025-11-19T20:51:12.400Z", "disclosures": [] } }, diff --git a/metadata/modules/adqueryBidAdapter.json b/metadata/modules/adqueryBidAdapter.json index adc84fa4f3e..567fc0e4041 100644 --- a/metadata/modules/adqueryBidAdapter.json +++ b/metadata/modules/adqueryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.adquery.io/tcf/adQuery.json": { - "timestamp": "2025-10-01T19:33:08.692Z", + "timestamp": "2025-11-19T20:51:12.421Z", "disclosures": [] } }, diff --git a/metadata/modules/adqueryIdSystem.json b/metadata/modules/adqueryIdSystem.json index 50454b12ab3..d8b870afe6b 100644 --- a/metadata/modules/adqueryIdSystem.json +++ b/metadata/modules/adqueryIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.adquery.io/tcf/adQuery.json": { - "timestamp": "2025-10-01T19:33:09.040Z", + "timestamp": "2025-11-19T20:51:12.756Z", "disclosures": [] } }, diff --git a/metadata/modules/adrinoBidAdapter.json b/metadata/modules/adrinoBidAdapter.json index b65dbe7a86e..2e3a762ddcd 100644 --- a/metadata/modules/adrinoBidAdapter.json +++ b/metadata/modules/adrinoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.adrino.cloud/iab/device-storage.json": { - "timestamp": "2025-10-01T19:33:09.041Z", + "timestamp": "2025-11-19T20:51:12.757Z", "disclosures": [] } }, diff --git a/metadata/modules/ads_interactiveBidAdapter.json b/metadata/modules/ads_interactiveBidAdapter.json index 384c2c68ad3..fd47c760862 100644 --- a/metadata/modules/ads_interactiveBidAdapter.json +++ b/metadata/modules/ads_interactiveBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adsinteractive.com/vendor.json": { - "timestamp": "2025-10-01T19:33:09.094Z", + "timestamp": "2025-11-19T20:51:12.854Z", "disclosures": [] } }, diff --git a/metadata/modules/adtargetBidAdapter.json b/metadata/modules/adtargetBidAdapter.json index 38cfe410619..4b559c80cdc 100644 --- a/metadata/modules/adtargetBidAdapter.json +++ b/metadata/modules/adtargetBidAdapter.json @@ -2,8 +2,14 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtarget.com.tr/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:09.413Z", - "disclosures": [] + "timestamp": "2025-11-19T20:51:13.161Z", + "disclosures": [ + { + "identifier": "adt_pbjs", + "type": "web", + "purposes": [] + } + ] } }, "components": [ diff --git a/metadata/modules/adtelligentBidAdapter.json b/metadata/modules/adtelligentBidAdapter.json index 4ecaabcb3c4..b3fb4a5db30 100644 --- a/metadata/modules/adtelligentBidAdapter.json +++ b/metadata/modules/adtelligentBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtelligent.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:09.413Z", + "timestamp": "2025-11-19T20:51:13.161Z", "disclosures": [] }, "https://www.selectmedia.asia/gdpr/devicestorage.json": { - "timestamp": "2025-10-01T19:33:09.432Z", + "timestamp": "2025-11-19T20:51:13.180Z", "disclosures": [ { "identifier": "waterFallCacheAnsKey_*", @@ -81,7 +81,7 @@ ] }, "https://orangeclickmedia.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:09.577Z", + "timestamp": "2025-11-19T20:51:13.354Z", "disclosures": [] } }, diff --git a/metadata/modules/adtelligentIdSystem.json b/metadata/modules/adtelligentIdSystem.json index 427d7319062..e62d3831e88 100644 --- a/metadata/modules/adtelligentIdSystem.json +++ b/metadata/modules/adtelligentIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adtelligent.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:09.632Z", + "timestamp": "2025-11-19T20:51:13.523Z", "disclosures": [] } }, diff --git a/metadata/modules/aduptechBidAdapter.json b/metadata/modules/aduptechBidAdapter.json index 2bc6d0a5480..ac02403710e 100644 --- a/metadata/modules/aduptechBidAdapter.json +++ b/metadata/modules/aduptechBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.d.adup-tech.com/gdpr/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:09.633Z", + "timestamp": "2025-11-19T20:51:13.524Z", "disclosures": [] } }, diff --git a/metadata/modules/adverxoBidAdapter.json b/metadata/modules/adverxoBidAdapter.json index 5e7eb1c2e31..cece61bdaf1 100644 --- a/metadata/modules/adverxoBidAdapter.json +++ b/metadata/modules/adverxoBidAdapter.json @@ -22,6 +22,13 @@ "aliasOf": "adverxo", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "harrenmedia", + "aliasOf": "adverxo", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/adyoulikeBidAdapter.json b/metadata/modules/adyoulikeBidAdapter.json index 056b21bde34..b4066e9b8ef 100644 --- a/metadata/modules/adyoulikeBidAdapter.json +++ b/metadata/modules/adyoulikeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adyoulike.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-10-01T19:33:09.660Z", + "timestamp": "2025-11-19T20:51:13.552Z", "disclosures": [] } }, diff --git a/metadata/modules/airgridRtdProvider.json b/metadata/modules/airgridRtdProvider.json index 083cce4a56b..46c58dddb5e 100644 --- a/metadata/modules/airgridRtdProvider.json +++ b/metadata/modules/airgridRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.wearemiq.com/privacy-and-compliance/devicestoragedisclosures.json": { - "timestamp": "2025-10-01T19:33:10.122Z", + "timestamp": "2025-11-19T20:51:14.020Z", "disclosures": [] } }, diff --git a/metadata/modules/alkimiBidAdapter.json b/metadata/modules/alkimiBidAdapter.json index fb12e7fcc5d..ad298eddabb 100644 --- a/metadata/modules/alkimiBidAdapter.json +++ b/metadata/modules/alkimiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://d1xjh92lb8fey3.cloudfront.net/tcf/alkimi_exchange_tcf.json": { - "timestamp": "2025-10-01T19:33:10.144Z", + "timestamp": "2025-11-19T20:51:14.058Z", "disclosures": [] } }, diff --git a/metadata/modules/amxBidAdapter.json b/metadata/modules/amxBidAdapter.json index 7f00724f9c3..0494a5ea70b 100644 --- a/metadata/modules/amxBidAdapter.json +++ b/metadata/modules/amxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.a-mo.net/tcf/device-storage.json": { - "timestamp": "2025-10-01T19:33:10.422Z", + "timestamp": "2025-11-19T20:51:14.354Z", "disclosures": [] } }, diff --git a/metadata/modules/amxIdSystem.json b/metadata/modules/amxIdSystem.json index e0a821d1905..77076a0455c 100644 --- a/metadata/modules/amxIdSystem.json +++ b/metadata/modules/amxIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.a-mo.net/tcf/device-storage.json": { - "timestamp": "2025-10-01T19:33:10.459Z", + "timestamp": "2025-11-19T20:51:14.408Z", "disclosures": [] } }, diff --git a/metadata/modules/aniviewBidAdapter.json b/metadata/modules/aniviewBidAdapter.json index 9cc9611ac03..fdc85537934 100644 --- a/metadata/modules/aniviewBidAdapter.json +++ b/metadata/modules/aniviewBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://player.aniview.com/gdpr/gdpr.json": { - "timestamp": "2025-10-01T19:33:10.460Z", + "timestamp": "2025-11-19T20:51:14.408Z", "disclosures": [ { "identifier": "av_*", diff --git a/metadata/modules/anonymisedRtdProvider.json b/metadata/modules/anonymisedRtdProvider.json index 68f6cf8665f..827805b370f 100644 --- a/metadata/modules/anonymisedRtdProvider.json +++ b/metadata/modules/anonymisedRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.anonymised.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:10.601Z", + "timestamp": "2025-11-19T20:51:14.519Z", "disclosures": [ { "identifier": "oidc.user*", @@ -40,6 +40,24 @@ 9, 10 ] + }, + { + "identifier": "anon-sl", + "type": "web", + "maxAgeSeconds": null, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "anon-hndshk", + "type": "web", + "maxAgeSeconds": null, + "cookieRefresh": false, + "purposes": [ + 1 + ] } ] } diff --git a/metadata/modules/appStockSSPBidAdapter.json b/metadata/modules/appStockSSPBidAdapter.json index 65a3c1b165d..6dd538cd5bf 100644 --- a/metadata/modules/appStockSSPBidAdapter.json +++ b/metadata/modules/appStockSSPBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://app-stock.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:10.622Z", + "timestamp": "2025-11-19T20:51:14.636Z", "disclosures": [] } }, diff --git a/metadata/modules/appierBidAdapter.json b/metadata/modules/appierBidAdapter.json index 581cad77167..831bda55c41 100644 --- a/metadata/modules/appierBidAdapter.json +++ b/metadata/modules/appierBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.appier.com/deviceStorage2025.json": { - "timestamp": "2025-10-01T19:33:10.647Z", + "timestamp": "2025-11-19T20:51:14.690Z", "disclosures": [ { "identifier": "_atrk_ssid", diff --git a/metadata/modules/appnexusBidAdapter.json b/metadata/modules/appnexusBidAdapter.json index 9082dd32a6b..99a201651de 100644 --- a/metadata/modules/appnexusBidAdapter.json +++ b/metadata/modules/appnexusBidAdapter.json @@ -2,23 +2,23 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { - "timestamp": "2025-10-01T19:33:11.366Z", + "timestamp": "2025-11-19T20:51:15.356Z", "disclosures": [] }, "https://tcf.emetriq.de/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:10.754Z", + "timestamp": "2025-11-19T20:51:14.828Z", "disclosures": [] }, "https://beintoo-support.b-cdn.net/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:10.801Z", + "timestamp": "2025-11-19T20:51:14.849Z", "disclosures": [] }, "https://projectagora.net/1032_deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:10.945Z", + "timestamp": "2025-11-19T20:51:14.865Z", "disclosures": [] }, "https://adzymic.com/tcf.json": { - "timestamp": "2025-10-01T19:33:11.366Z", + "timestamp": "2025-11-19T20:51:15.356Z", "disclosures": [] } }, diff --git a/metadata/modules/appushBidAdapter.json b/metadata/modules/appushBidAdapter.json index 024d91ab268..c0935899043 100644 --- a/metadata/modules/appushBidAdapter.json +++ b/metadata/modules/appushBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.thebiding.com/disclosures.json": { - "timestamp": "2025-10-01T19:33:11.405Z", + "timestamp": "2025-11-19T20:51:15.414Z", "disclosures": [] } }, diff --git a/metadata/modules/apstreamBidAdapter.json b/metadata/modules/apstreamBidAdapter.json index ff515989ef7..86ba9c1dc17 100644 --- a/metadata/modules/apstreamBidAdapter.json +++ b/metadata/modules/apstreamBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sak.userreport.com/tcf.json": { - "timestamp": "2025-10-01T19:33:11.470Z", + "timestamp": "2025-11-19T20:51:15.528Z", "disclosures": [ { "identifier": "apr_dsu", diff --git a/metadata/modules/audiencerunBidAdapter.json b/metadata/modules/audiencerunBidAdapter.json index acb14e007cc..d308af1c05b 100644 --- a/metadata/modules/audiencerunBidAdapter.json +++ b/metadata/modules/audiencerunBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.audiencerun.com/tcf.json": { - "timestamp": "2025-10-01T19:33:11.491Z", + "timestamp": "2025-11-19T20:51:15.551Z", "disclosures": [] } }, diff --git a/metadata/modules/axisBidAdapter.json b/metadata/modules/axisBidAdapter.json index 96a82498b2e..119c83c1d1a 100644 --- a/metadata/modules/axisBidAdapter.json +++ b/metadata/modules/axisBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://axis-marketplace.com/tcf.json": { - "timestamp": "2025-10-01T19:33:11.531Z", + "timestamp": "2025-11-19T20:51:15.615Z", "disclosures": [] } }, diff --git a/metadata/modules/azerionedgeRtdProvider.json b/metadata/modules/azerionedgeRtdProvider.json index af56f72fd51..9dacb4aca40 100644 --- a/metadata/modules/azerionedgeRtdProvider.json +++ b/metadata/modules/azerionedgeRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sellers.improvedigital.com/tcf-cookies.json": { - "timestamp": "2025-10-01T19:33:11.569Z", + "timestamp": "2025-11-19T20:51:15.658Z", "disclosures": [ { "identifier": "tuuid", diff --git a/metadata/modules/beachfrontBidAdapter.json b/metadata/modules/beachfrontBidAdapter.json index 9d39c3c457e..63d6f32a7c8 100644 --- a/metadata/modules/beachfrontBidAdapter.json +++ b/metadata/modules/beachfrontBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.seedtag.com/vendor.json": { - "timestamp": "2025-10-01T19:33:11.597Z", + "timestamp": "2025-11-19T20:51:15.684Z", "disclosures": [] } }, diff --git a/metadata/modules/beopBidAdapter.json b/metadata/modules/beopBidAdapter.json index 6f3dc6dad77..3fd5c0a4a8d 100644 --- a/metadata/modules/beopBidAdapter.json +++ b/metadata/modules/beopBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://beop.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:11.733Z", + "timestamp": "2025-11-19T20:51:16.048Z", "disclosures": [] } }, diff --git a/metadata/modules/betweenBidAdapter.json b/metadata/modules/betweenBidAdapter.json index d7bc5522739..36747536871 100644 --- a/metadata/modules/betweenBidAdapter.json +++ b/metadata/modules/betweenBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://en.betweenx.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:11.854Z", + "timestamp": "2025-11-19T20:51:16.175Z", "disclosures": [] } }, diff --git a/metadata/modules/bidfuseBidAdapter.json b/metadata/modules/bidfuseBidAdapter.json index d541d6d0177..0f1ca2fa3fa 100644 --- a/metadata/modules/bidfuseBidAdapter.json +++ b/metadata/modules/bidfuseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bidfuse.com/disclosure.json": { - "timestamp": "2025-10-01T19:33:11.888Z", + "timestamp": "2025-11-19T20:51:16.229Z", "disclosures": [] } }, diff --git a/metadata/modules/bidmaticBidAdapter.json b/metadata/modules/bidmaticBidAdapter.json index 6c9e424d949..465dd2cbdfe 100644 --- a/metadata/modules/bidmaticBidAdapter.json +++ b/metadata/modules/bidmaticBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bidmatic.io/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:12.083Z", + "timestamp": "2025-11-19T20:51:16.409Z", "disclosures": [] } }, diff --git a/metadata/modules/bidtheatreBidAdapter.json b/metadata/modules/bidtheatreBidAdapter.json index 94a6872f28c..371cf75c451 100644 --- a/metadata/modules/bidtheatreBidAdapter.json +++ b/metadata/modules/bidtheatreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.bidtheatre.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:12.098Z", + "timestamp": "2025-11-19T20:51:16.457Z", "disclosures": [] } }, diff --git a/metadata/modules/bliinkBidAdapter.json b/metadata/modules/bliinkBidAdapter.json index 9816a10c9d8..e04e63b647b 100644 --- a/metadata/modules/bliinkBidAdapter.json +++ b/metadata/modules/bliinkBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bliink.io/disclosures.json": { - "timestamp": "2025-10-01T19:33:12.385Z", + "timestamp": "2025-11-19T20:51:16.779Z", "disclosures": [] } }, diff --git a/metadata/modules/blockthroughBidAdapter.json b/metadata/modules/blockthroughBidAdapter.json index 19fbc73677f..54fd5fb8aaf 100644 --- a/metadata/modules/blockthroughBidAdapter.json +++ b/metadata/modules/blockthroughBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://blockthrough.com/tcf_disclosures.json": { - "timestamp": "2025-10-01T19:33:12.708Z", + "timestamp": "2025-11-10T11:41:19.544Z", "disclosures": [ { "identifier": "BT_AA_DETECTION", diff --git a/metadata/modules/blueBidAdapter.json b/metadata/modules/blueBidAdapter.json index 9c52fbe59a1..a0de664de61 100644 --- a/metadata/modules/blueBidAdapter.json +++ b/metadata/modules/blueBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://getblue.io/iab/iab.json": { - "timestamp": "2025-10-01T19:33:12.818Z", + "timestamp": "2025-11-19T20:51:17.219Z", "disclosures": [] } }, diff --git a/metadata/modules/bmsBidAdapter.json b/metadata/modules/bmsBidAdapter.json index c16713ff497..c3f5aae4563 100644 --- a/metadata/modules/bmsBidAdapter.json +++ b/metadata/modules/bmsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bluems.com/iab.json": { - "timestamp": "2025-10-01T19:33:13.188Z", + "timestamp": "2025-11-19T20:51:17.572Z", "disclosures": [] } }, diff --git a/metadata/modules/boldwinBidAdapter.json b/metadata/modules/boldwinBidAdapter.json index a4399c223bc..75dd1c781be 100644 --- a/metadata/modules/boldwinBidAdapter.json +++ b/metadata/modules/boldwinBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://magav.videowalldirect.com/iab/videowalldirectiab.json": { - "timestamp": "2025-10-01T19:33:13.205Z", + "timestamp": "2025-11-19T20:51:17.595Z", "disclosures": [] } }, diff --git a/metadata/modules/bridBidAdapter.json b/metadata/modules/bridBidAdapter.json index 58d95ad1a6c..597e7796973 100644 --- a/metadata/modules/bridBidAdapter.json +++ b/metadata/modules/bridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://target-video.com/vendors-device-storage-and-operational-disclosures.json": { - "timestamp": "2025-10-01T19:33:13.226Z", + "timestamp": "2025-11-19T20:51:17.700Z", "disclosures": [ { "identifier": "brid_location", diff --git a/metadata/modules/browsiBidAdapter.json b/metadata/modules/browsiBidAdapter.json index a8ed1849a78..d81cca591e1 100644 --- a/metadata/modules/browsiBidAdapter.json +++ b/metadata/modules/browsiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.browsiprod.com/ads/tcf.json": { - "timestamp": "2025-10-01T19:33:13.367Z", + "timestamp": "2025-11-19T20:51:17.840Z", "disclosures": [] } }, diff --git a/metadata/modules/bucksenseBidAdapter.json b/metadata/modules/bucksenseBidAdapter.json index 854cbfa91c4..8d71736c557 100644 --- a/metadata/modules/bucksenseBidAdapter.json +++ b/metadata/modules/bucksenseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://j.bksnimages.com/iab/devsto02.json": { - "timestamp": "2025-10-01T19:33:13.413Z", + "timestamp": "2025-11-19T20:51:17.857Z", "disclosures": [] } }, diff --git a/metadata/modules/carodaBidAdapter.json b/metadata/modules/carodaBidAdapter.json index bc0fb4a755c..5c565ba1aa8 100644 --- a/metadata/modules/carodaBidAdapter.json +++ b/metadata/modules/carodaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn2.caroda.io/tcfvds/2022-05-17/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:13.450Z", + "timestamp": "2025-11-19T20:51:17.919Z", "disclosures": [] } }, diff --git a/metadata/modules/categoryTranslation.json b/metadata/modules/categoryTranslation.json index 205f8ac5a7b..921493ce0fb 100644 --- a/metadata/modules/categoryTranslation.json +++ b/metadata/modules/categoryTranslation.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/categoryTranslation.json": { - "timestamp": "2025-10-01T19:33:04.697Z", + "timestamp": "2025-11-19T20:51:08.094Z", "disclosures": [ { "identifier": "iabToFwMappingkey", diff --git a/metadata/modules/ceeIdSystem.json b/metadata/modules/ceeIdSystem.json index c8dc4168ce0..307b7549665 100644 --- a/metadata/modules/ceeIdSystem.json +++ b/metadata/modules/ceeIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.wp.pl/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:13.626Z", + "timestamp": "2025-11-19T20:51:18.230Z", "disclosures": null } }, diff --git a/metadata/modules/chromeAiRtdProvider.json b/metadata/modules/chromeAiRtdProvider.json index d47fd72c832..16accf9cae2 100644 --- a/metadata/modules/chromeAiRtdProvider.json +++ b/metadata/modules/chromeAiRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/chromeAiRtdProvider.json": { - "timestamp": "2025-10-01T19:33:14.008Z", + "timestamp": "2025-11-19T20:51:18.581Z", "disclosures": [ { "identifier": "chromeAi_detected_data", diff --git a/metadata/modules/clickioBidAdapter.json b/metadata/modules/clickioBidAdapter.json new file mode 100644 index 00000000000..de263bb0585 --- /dev/null +++ b/metadata/modules/clickioBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "clickio", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/compassBidAdapter.json b/metadata/modules/compassBidAdapter.json index 35ed86c3874..925c8facc3f 100644 --- a/metadata/modules/compassBidAdapter.json +++ b/metadata/modules/compassBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.marphezis.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-10-01T19:33:14.011Z", + "timestamp": "2025-11-19T20:51:18.583Z", "disclosures": [] } }, diff --git a/metadata/modules/conceptxBidAdapter.json b/metadata/modules/conceptxBidAdapter.json index bf4e43090f9..edbf991fc56 100644 --- a/metadata/modules/conceptxBidAdapter.json +++ b/metadata/modules/conceptxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cncptx.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:14.026Z", + "timestamp": "2025-11-19T20:51:18.599Z", "disclosures": [] } }, diff --git a/metadata/modules/connatixBidAdapter.json b/metadata/modules/connatixBidAdapter.json index ed78bb43beb..9a4634d837c 100644 --- a/metadata/modules/connatixBidAdapter.json +++ b/metadata/modules/connatixBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://connatix.com/iab-tcf-disclosure.json": { - "timestamp": "2025-10-01T19:33:14.046Z", + "timestamp": "2025-11-19T20:51:18.626Z", "disclosures": [ { "identifier": "cnx_userId", diff --git a/metadata/modules/connectIdSystem.json b/metadata/modules/connectIdSystem.json index be28c5be92b..92943284539 100644 --- a/metadata/modules/connectIdSystem.json +++ b/metadata/modules/connectIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://meta.legal.yahoo.com/iab-tcf/v2/device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:14.115Z", + "timestamp": "2025-11-19T20:51:18.722Z", "disclosures": [ { "identifier": "vmcid", diff --git a/metadata/modules/connectadBidAdapter.json b/metadata/modules/connectadBidAdapter.json index 679a1791bfb..474b3ab83eb 100644 --- a/metadata/modules/connectadBidAdapter.json +++ b/metadata/modules/connectadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.connectad.io/tcf_storage_info.json": { - "timestamp": "2025-10-01T19:33:14.134Z", + "timestamp": "2025-11-19T20:51:18.745Z", "disclosures": [] } }, diff --git a/metadata/modules/contentexchangeBidAdapter.json b/metadata/modules/contentexchangeBidAdapter.json index 3cc1c860568..d53b22757e2 100644 --- a/metadata/modules/contentexchangeBidAdapter.json +++ b/metadata/modules/contentexchangeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://hb.contentexchange.me/template/device_storage.json": { - "timestamp": "2025-10-01T19:33:14.558Z", + "timestamp": "2025-11-10T11:41:21.385Z", "disclosures": [] } }, diff --git a/metadata/modules/conversantBidAdapter.json b/metadata/modules/conversantBidAdapter.json index 83777529def..5cab777a157 100644 --- a/metadata/modules/conversantBidAdapter.json +++ b/metadata/modules/conversantBidAdapter.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://s-usweb.dotomi.com/assets/js/taggy-js/2.17.0/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:14.957Z", + "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.8/device_storage_disclosure.json": { + "timestamp": "2025-11-19T20:51:18.822Z", "disclosures": [ { "identifier": "dtm_status", @@ -441,6 +441,44 @@ 11 ] }, + { + "identifier": "_pubcid_exp", + "type": "web", + "maxAgeSeconds": null, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, + { + "identifier": "__dtmtest_*", + "type": "cookie", + "maxAgeSeconds": 60, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, { "identifier": "_rl_aud", "type": "cookie", @@ -516,6 +554,25 @@ 10, 11 ] + }, + { + "identifier": "hConversionEventId", + "type": "cookie", + "maxAgeSeconds": 2592000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] } ] } @@ -526,7 +583,7 @@ "componentName": "conversant", "aliasOf": null, "gvlid": 24, - "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.17.0/device_storage_disclosure.json" + "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.8/device_storage_disclosure.json" }, { "componentType": "bidder", diff --git a/metadata/modules/copper6sspBidAdapter.json b/metadata/modules/copper6sspBidAdapter.json index 102afd653e0..164f7677af4 100644 --- a/metadata/modules/copper6sspBidAdapter.json +++ b/metadata/modules/copper6sspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.copper6.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:14.971Z", + "timestamp": "2025-11-19T20:51:18.845Z", "disclosures": [] } }, diff --git a/metadata/modules/cpmstarBidAdapter.json b/metadata/modules/cpmstarBidAdapter.json index bcc7ddb059f..9b49d8dfb84 100644 --- a/metadata/modules/cpmstarBidAdapter.json +++ b/metadata/modules/cpmstarBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.aditude.com/storageaccess.json": { - "timestamp": "2025-10-01T19:33:15.059Z", + "timestamp": "2025-11-19T20:51:18.971Z", "disclosures": [] } }, diff --git a/metadata/modules/criteoBidAdapter.json b/metadata/modules/criteoBidAdapter.json index 46529cb54b2..c405acba13c 100644 --- a/metadata/modules/criteoBidAdapter.json +++ b/metadata/modules/criteoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.criteo.com/iab-europe/tcfv2/disclosure": { - "timestamp": "2025-10-01T19:33:15.136Z", + "timestamp": "2025-11-19T20:51:19.021Z", "disclosures": [ { "identifier": "criteo_fast_bid", diff --git a/metadata/modules/criteoIdSystem.json b/metadata/modules/criteoIdSystem.json index 95edc1557d1..cf3766536e9 100644 --- a/metadata/modules/criteoIdSystem.json +++ b/metadata/modules/criteoIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.criteo.com/iab-europe/tcfv2/disclosure": { - "timestamp": "2025-10-01T19:33:15.152Z", + "timestamp": "2025-11-19T20:51:19.041Z", "disclosures": [ { "identifier": "criteo_fast_bid", diff --git a/metadata/modules/cwireBidAdapter.json b/metadata/modules/cwireBidAdapter.json index bd455aaed92..fb9e068e4e3 100644 --- a/metadata/modules/cwireBidAdapter.json +++ b/metadata/modules/cwireBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.cwi.re/artifacts/iab/iab.json": { - "timestamp": "2025-10-01T19:33:15.153Z", + "timestamp": "2025-11-19T20:51:19.041Z", "disclosures": [] } }, diff --git a/metadata/modules/czechAdIdSystem.json b/metadata/modules/czechAdIdSystem.json index dcac63167ca..e4b953bfb59 100644 --- a/metadata/modules/czechAdIdSystem.json +++ b/metadata/modules/czechAdIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cpex.cz/storagedisclosure.json": { - "timestamp": "2025-10-01T19:33:15.173Z", + "timestamp": "2025-11-19T20:51:19.388Z", "disclosures": [] } }, diff --git a/metadata/modules/dailymotionBidAdapter.json b/metadata/modules/dailymotionBidAdapter.json index c85068c76b5..c48795f0a1e 100644 --- a/metadata/modules/dailymotionBidAdapter.json +++ b/metadata/modules/dailymotionBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://statics.dmcdn.net/a/vds.json": { - "timestamp": "2025-10-01T19:33:15.575Z", + "timestamp": "2025-11-19T20:51:19.691Z", "disclosures": [ { "identifier": "uid_dm", diff --git a/metadata/modules/debugging.json b/metadata/modules/debugging.json index a2e90db432c..00eed3703dd 100644 --- a/metadata/modules/debugging.json +++ b/metadata/modules/debugging.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/debugging.json": { - "timestamp": "2025-10-01T19:33:04.696Z", + "timestamp": "2025-11-19T20:51:08.093Z", "disclosures": [ { "identifier": "__*_debugging__", diff --git a/metadata/modules/deepintentBidAdapter.json b/metadata/modules/deepintentBidAdapter.json index f425178616f..8b38c30277a 100644 --- a/metadata/modules/deepintentBidAdapter.json +++ b/metadata/modules/deepintentBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.deepintent.com/iabeurope_vendor_disclosures.json": { - "timestamp": "2025-10-01T19:33:15.672Z", + "timestamp": "2025-11-19T20:51:19.711Z", "disclosures": [] } }, diff --git a/metadata/modules/defineMediaBidAdapter.json b/metadata/modules/defineMediaBidAdapter.json new file mode 100644 index 00000000000..95a5a4ccfc6 --- /dev/null +++ b/metadata/modules/defineMediaBidAdapter.json @@ -0,0 +1,36 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://definemedia.de/tcf/deviceStorageDisclosureURL.json": { + "timestamp": "2025-11-19T20:51:20.094Z", + "disclosures": [ + { + "identifier": "conative$dataGathering$Adex", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1, + 2 + ] + }, + { + "identifier": "conative$proddataGathering$ContextId$*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + } + ] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "defineMedia", + "aliasOf": null, + "gvlid": 440, + "disclosureURL": "https://definemedia.de/tcf/deviceStorageDisclosureURL.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/deltaprojectsBidAdapter.json b/metadata/modules/deltaprojectsBidAdapter.json index d1a936e7626..02545785549 100644 --- a/metadata/modules/deltaprojectsBidAdapter.json +++ b/metadata/modules/deltaprojectsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.de17a.com/policy/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:15.738Z", + "timestamp": "2025-11-19T20:51:20.507Z", "disclosures": [] } }, diff --git a/metadata/modules/dianomiBidAdapter.json b/metadata/modules/dianomiBidAdapter.json index 7dc6bdddbff..7131f0c4f2c 100644 --- a/metadata/modules/dianomiBidAdapter.json +++ b/metadata/modules/dianomiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.dianomi.com/device_storage.json": { - "timestamp": "2025-10-01T19:33:16.174Z", + "timestamp": "2025-11-19T20:51:20.947Z", "disclosures": [] } }, diff --git a/metadata/modules/digitalMatterBidAdapter.json b/metadata/modules/digitalMatterBidAdapter.json index 5cea07d775a..b3999a2f058 100644 --- a/metadata/modules/digitalMatterBidAdapter.json +++ b/metadata/modules/digitalMatterBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://digitalmatter.ai/disclosures.json": { - "timestamp": "2025-10-01T19:33:16.174Z", + "timestamp": "2025-11-19T20:51:20.947Z", "disclosures": [] } }, diff --git a/metadata/modules/distroscaleBidAdapter.json b/metadata/modules/distroscaleBidAdapter.json index a4883681b6f..f147261d7b4 100644 --- a/metadata/modules/distroscaleBidAdapter.json +++ b/metadata/modules/distroscaleBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://a.jsrdn.com/tcf/tcf-vendor-disclosure.json": { - "timestamp": "2025-10-01T19:33:16.574Z", + "timestamp": "2025-11-19T20:51:21.331Z", "disclosures": [] } }, diff --git a/metadata/modules/docereeAdManagerBidAdapter.json b/metadata/modules/docereeAdManagerBidAdapter.json index 220a82c90d6..11b3ddc6a64 100644 --- a/metadata/modules/docereeAdManagerBidAdapter.json +++ b/metadata/modules/docereeAdManagerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://doceree.com/.well-known/iab/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:16.672Z", + "timestamp": "2025-11-19T20:51:21.358Z", "disclosures": [] } }, diff --git a/metadata/modules/docereeBidAdapter.json b/metadata/modules/docereeBidAdapter.json index 82b8e6ad547..d120fbe6044 100644 --- a/metadata/modules/docereeBidAdapter.json +++ b/metadata/modules/docereeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://doceree.com/.well-known/iab/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:17.423Z", + "timestamp": "2025-11-19T20:51:22.114Z", "disclosures": [] } }, diff --git a/metadata/modules/dspxBidAdapter.json b/metadata/modules/dspxBidAdapter.json index 03c85367cff..fa0a5901037 100644 --- a/metadata/modules/dspxBidAdapter.json +++ b/metadata/modules/dspxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.adtech.app/gen/deviceStorageDisclosure/os.json": { - "timestamp": "2025-10-01T19:33:17.423Z", + "timestamp": "2025-11-19T20:51:22.115Z", "disclosures": [] } }, diff --git a/metadata/modules/e_volutionBidAdapter.json b/metadata/modules/e_volutionBidAdapter.json index 946701b66fa..e4c50af4c11 100644 --- a/metadata/modules/e_volutionBidAdapter.json +++ b/metadata/modules/e_volutionBidAdapter.json @@ -2,8 +2,8 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://e-volution.ai/file.json": { - "timestamp": "2025-10-01T19:33:18.177Z", - "disclosures": [] + "timestamp": "2025-11-19T20:51:22.794Z", + "disclosures": null } }, "components": [ diff --git a/metadata/modules/edge226BidAdapter.json b/metadata/modules/edge226BidAdapter.json index 8c24adb5cb4..b73461bb162 100644 --- a/metadata/modules/edge226BidAdapter.json +++ b/metadata/modules/edge226BidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.serveteck.com/cdn_storage/tcf/tcf.json?a=1.io": { - "timestamp": "2025-10-01T19:33:18.210Z", + "timestamp": "2025-11-19T20:51:23.104Z", "disclosures": [] } }, diff --git a/metadata/modules/empowerBidAdapter.json b/metadata/modules/empowerBidAdapter.json new file mode 100644 index 00000000000..03faa868057 --- /dev/null +++ b/metadata/modules/empowerBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://cdn.empower.net/vendor/vendor.json": { + "timestamp": "2025-11-19T20:51:23.177Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "empower", + "aliasOf": null, + "gvlid": 1248, + "disclosureURL": "https://cdn.empower.net/vendor/vendor.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/equativBidAdapter.json b/metadata/modules/equativBidAdapter.json index c959e4b884f..ac701e3462c 100644 --- a/metadata/modules/equativBidAdapter.json +++ b/metadata/modules/equativBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://apps.smartadserver.com/device-storage-disclosures/equativDeviceStorageDisclosures.json": { - "timestamp": "2025-10-01T19:33:18.290Z", + "timestamp": "2025-11-19T20:51:23.210Z", "disclosures": [] } }, diff --git a/metadata/modules/eskimiBidAdapter.json b/metadata/modules/eskimiBidAdapter.json index 5225b28b177..f3f5575ceeb 100644 --- a/metadata/modules/eskimiBidAdapter.json +++ b/metadata/modules/eskimiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://dsp-media.eskimi.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:18.968Z", + "timestamp": "2025-11-19T20:51:23.853Z", "disclosures": [] } }, diff --git a/metadata/modules/etargetBidAdapter.json b/metadata/modules/etargetBidAdapter.json index 3663e8aa7e5..eb2ef4ed708 100644 --- a/metadata/modules/etargetBidAdapter.json +++ b/metadata/modules/etargetBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.etarget.sk/cookies3.json": { - "timestamp": "2025-10-01T19:33:18.995Z", + "timestamp": "2025-11-19T20:51:23.873Z", "disclosures": [] } }, diff --git a/metadata/modules/euidIdSystem.json b/metadata/modules/euidIdSystem.json index 0131e72cfec..430d3c77873 100644 --- a/metadata/modules/euidIdSystem.json +++ b/metadata/modules/euidIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-10-01T19:33:19.557Z", + "timestamp": "2025-11-19T20:51:24.550Z", "disclosures": [] } }, diff --git a/metadata/modules/exadsBidAdapter.json b/metadata/modules/exadsBidAdapter.json index 3687fb40fcc..b93abf1f5aa 100644 --- a/metadata/modules/exadsBidAdapter.json +++ b/metadata/modules/exadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://a.native7.com/tcf/deviceStorage.php": { - "timestamp": "2025-10-01T19:33:19.779Z", + "timestamp": "2025-11-19T20:51:24.771Z", "disclosures": [ { "identifier": "pn-zone-*", diff --git a/metadata/modules/feedadBidAdapter.json b/metadata/modules/feedadBidAdapter.json index dcfefe68853..6385f9ed012 100644 --- a/metadata/modules/feedadBidAdapter.json +++ b/metadata/modules/feedadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://api.feedad.com/tcf-device-disclosures.json": { - "timestamp": "2025-10-01T19:33:19.978Z", + "timestamp": "2025-11-19T20:51:24.973Z", "disclosures": [ { "identifier": "__fad_data", diff --git a/metadata/modules/fwsspBidAdapter.json b/metadata/modules/fwsspBidAdapter.json index 261656cda83..5a38f86edb4 100644 --- a/metadata/modules/fwsspBidAdapter.json +++ b/metadata/modules/fwsspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab.fwmrm.net/g/devicedisclosure.json": { - "timestamp": "2025-10-01T19:33:20.144Z", + "timestamp": "2025-11-19T20:51:25.329Z", "disclosures": [] } }, diff --git a/metadata/modules/gamoshiBidAdapter.json b/metadata/modules/gamoshiBidAdapter.json index 296a4d65767..b72069e034e 100644 --- a/metadata/modules/gamoshiBidAdapter.json +++ b/metadata/modules/gamoshiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.gamoshi.com/disclosures-client-storage.json": { - "timestamp": "2025-10-01T19:33:20.321Z", + "timestamp": "2025-11-19T20:51:25.921Z", "disclosures": [] } }, diff --git a/metadata/modules/gemiusIdSystem.json b/metadata/modules/gemiusIdSystem.json index 383c9650b9a..fb4dfba197d 100644 --- a/metadata/modules/gemiusIdSystem.json +++ b/metadata/modules/gemiusIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gemius.com/media/documents/Gemius_SA_Vendor_Device_Storage.json": { - "timestamp": "2025-10-01T19:33:20.399Z", + "timestamp": "2025-11-19T20:51:26.097Z", "disclosures": [ { "identifier": "__gsyncs_gdpr", diff --git a/metadata/modules/glomexBidAdapter.json b/metadata/modules/glomexBidAdapter.json index 24407f3296d..5df2c608934 100644 --- a/metadata/modules/glomexBidAdapter.json +++ b/metadata/modules/glomexBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://player.glomex.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:20.958Z", + "timestamp": "2025-11-19T20:51:26.671Z", "disclosures": [ { "identifier": "glomexUser", diff --git a/metadata/modules/goldbachBidAdapter.json b/metadata/modules/goldbachBidAdapter.json index 1970dd486fb..1104c937c2d 100644 --- a/metadata/modules/goldbachBidAdapter.json +++ b/metadata/modules/goldbachBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gb-next.ch/TcfGoldbachDeviceStorage.json": { - "timestamp": "2025-10-01T19:33:20.977Z", + "timestamp": "2025-11-19T20:51:26.704Z", "disclosures": [ { "identifier": "dakt_2_session_id", diff --git a/metadata/modules/gridBidAdapter.json b/metadata/modules/gridBidAdapter.json index b658deac3e5..b3866edd805 100644 --- a/metadata/modules/gridBidAdapter.json +++ b/metadata/modules/gridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.themediagrid.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:21.097Z", + "timestamp": "2025-11-19T20:51:26.766Z", "disclosures": [] } }, diff --git a/metadata/modules/gumgumBidAdapter.json b/metadata/modules/gumgumBidAdapter.json index 0b373a70ed4..5067cac4039 100644 --- a/metadata/modules/gumgumBidAdapter.json +++ b/metadata/modules/gumgumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://marketing.gumgum.com/devicestoragedisclosures.json": { - "timestamp": "2025-10-01T19:33:21.180Z", + "timestamp": "2025-11-19T20:51:26.916Z", "disclosures": [] } }, diff --git a/metadata/modules/hadronIdSystem.json b/metadata/modules/hadronIdSystem.json index 65554d762a7..5930b2bfbd5 100644 --- a/metadata/modules/hadronIdSystem.json +++ b/metadata/modules/hadronIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://p.ad.gt/static/iab_tcf.json": { - "timestamp": "2025-10-01T19:33:21.287Z", + "timestamp": "2025-11-19T20:51:26.988Z", "disclosures": [ { "identifier": "au/sid", diff --git a/metadata/modules/hadronRtdProvider.json b/metadata/modules/hadronRtdProvider.json index 4e27ae42893..9d4a1d5dfd2 100644 --- a/metadata/modules/hadronRtdProvider.json +++ b/metadata/modules/hadronRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://p.ad.gt/static/iab_tcf.json": { - "timestamp": "2025-10-01T19:33:21.394Z", + "timestamp": "2025-11-19T20:51:27.109Z", "disclosures": [ { "identifier": "au/sid", diff --git a/metadata/modules/holidBidAdapter.json b/metadata/modules/holidBidAdapter.json index e365021f51e..24eacb9bc79 100644 --- a/metadata/modules/holidBidAdapter.json +++ b/metadata/modules/holidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ads.holid.io/devicestorage.json": { - "timestamp": "2025-10-01T19:33:21.395Z", + "timestamp": "2025-11-19T20:51:27.109Z", "disclosures": [ { "identifier": "uids", diff --git a/metadata/modules/hybridBidAdapter.json b/metadata/modules/hybridBidAdapter.json index 51ec54d56de..29aab4a9ff7 100644 --- a/metadata/modules/hybridBidAdapter.json +++ b/metadata/modules/hybridBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://st.hybrid.ai/policy/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:21.632Z", + "timestamp": "2025-11-19T20:51:27.382Z", "disclosures": [] } }, diff --git a/metadata/modules/id5IdSystem.json b/metadata/modules/id5IdSystem.json index 17136e27438..bbc919ef861 100644 --- a/metadata/modules/id5IdSystem.json +++ b/metadata/modules/id5IdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://id5-sync.com/tcf/disclosures.json": { - "timestamp": "2025-10-01T19:33:21.786Z", + "timestamp": "2025-11-19T20:51:27.667Z", "disclosures": [] } }, diff --git a/metadata/modules/identityLinkIdSystem.json b/metadata/modules/identityLinkIdSystem.json index 7dcf48ac78c..1e959d69d9b 100644 --- a/metadata/modules/identityLinkIdSystem.json +++ b/metadata/modules/identityLinkIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.ats.rlcdn.com/device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:22.055Z", + "timestamp": "2025-11-19T20:51:27.944Z", "disclosures": [ { "identifier": "_lr_retry_request", diff --git a/metadata/modules/illuminBidAdapter.json b/metadata/modules/illuminBidAdapter.json index 3a11e74c711..f75633254dd 100644 --- a/metadata/modules/illuminBidAdapter.json +++ b/metadata/modules/illuminBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://admanmedia.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:22.074Z", + "timestamp": "2025-11-19T20:51:27.970Z", "disclosures": [] } }, diff --git a/metadata/modules/impactifyBidAdapter.json b/metadata/modules/impactifyBidAdapter.json index 027bf208987..de6ab2e3d5e 100644 --- a/metadata/modules/impactifyBidAdapter.json +++ b/metadata/modules/impactifyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.impactify.io/tcfvendors.json": { - "timestamp": "2025-10-01T19:33:22.363Z", + "timestamp": "2025-11-19T20:51:28.316Z", "disclosures": [ { "identifier": "_im*", diff --git a/metadata/modules/improvedigitalBidAdapter.json b/metadata/modules/improvedigitalBidAdapter.json index 2f96f518129..7da0a1c40e4 100644 --- a/metadata/modules/improvedigitalBidAdapter.json +++ b/metadata/modules/improvedigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sellers.improvedigital.com/tcf-cookies.json": { - "timestamp": "2025-10-01T19:33:22.670Z", + "timestamp": "2025-11-19T20:51:28.592Z", "disclosures": [ { "identifier": "tuuid", diff --git a/metadata/modules/inmobiBidAdapter.json b/metadata/modules/inmobiBidAdapter.json index 16b2d0f470d..bade3996e48 100644 --- a/metadata/modules/inmobiBidAdapter.json +++ b/metadata/modules/inmobiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://publisher.inmobi.com/public/disclosure": { - "timestamp": "2025-10-01T19:33:22.671Z", + "timestamp": "2025-11-19T20:51:28.593Z", "disclosures": [] } }, diff --git a/metadata/modules/insticatorBidAdapter.json b/metadata/modules/insticatorBidAdapter.json index c09dfba97c0..9678d9143ca 100644 --- a/metadata/modules/insticatorBidAdapter.json +++ b/metadata/modules/insticatorBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.insticator.com/iab/device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:22.701Z", + "timestamp": "2025-11-19T20:51:28.624Z", "disclosures": [ { "identifier": "visitorGeo", diff --git a/metadata/modules/intentIqIdSystem.json b/metadata/modules/intentIqIdSystem.json index bc597007904..7dfe9d8f578 100644 --- a/metadata/modules/intentIqIdSystem.json +++ b/metadata/modules/intentIqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://agent.intentiq.com/GDPR/gdpr.json": { - "timestamp": "2025-10-01T19:33:22.734Z", + "timestamp": "2025-11-19T20:51:28.651Z", "disclosures": [] } }, diff --git a/metadata/modules/invibesBidAdapter.json b/metadata/modules/invibesBidAdapter.json index 05e52c7d168..7ea4211bf8e 100644 --- a/metadata/modules/invibesBidAdapter.json +++ b/metadata/modules/invibesBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.invibes.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:22.786Z", + "timestamp": "2025-11-19T20:51:28.710Z", "disclosures": [ { "identifier": "ivvcap", diff --git a/metadata/modules/ipromBidAdapter.json b/metadata/modules/ipromBidAdapter.json index 3884c42db35..cfd02b7360c 100644 --- a/metadata/modules/ipromBidAdapter.json +++ b/metadata/modules/ipromBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://core.iprom.net/info/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:23.233Z", + "timestamp": "2025-11-19T20:51:29.106Z", "disclosures": [] } }, diff --git a/metadata/modules/ixBidAdapter.json b/metadata/modules/ixBidAdapter.json index 99f28625d09..b45469e19ec 100644 --- a/metadata/modules/ixBidAdapter.json +++ b/metadata/modules/ixBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.indexexchange.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:23.702Z", + "timestamp": "2025-11-19T20:51:29.586Z", "disclosures": [ { "identifier": "ix_features", diff --git a/metadata/modules/justIdSystem.json b/metadata/modules/justIdSystem.json index 317512d4a11..8526c632c84 100644 --- a/metadata/modules/justIdSystem.json +++ b/metadata/modules/justIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://audience-solutions.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:23.832Z", + "timestamp": "2025-11-19T20:51:29.770Z", "disclosures": [ { "identifier": "__jtuid", diff --git a/metadata/modules/justpremiumBidAdapter.json b/metadata/modules/justpremiumBidAdapter.json index fa0ddee0a2b..c7e37ae5747 100644 --- a/metadata/modules/justpremiumBidAdapter.json +++ b/metadata/modules/justpremiumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.justpremium.com/devicestoragedisclosures.json": { - "timestamp": "2025-10-01T19:33:24.347Z", + "timestamp": "2025-11-19T20:51:30.293Z", "disclosures": [] } }, diff --git a/metadata/modules/jwplayerBidAdapter.json b/metadata/modules/jwplayerBidAdapter.json index 6cd95b62b68..03601c43bf2 100644 --- a/metadata/modules/jwplayerBidAdapter.json +++ b/metadata/modules/jwplayerBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.jwplayer.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:24.368Z", + "timestamp": "2025-11-19T20:51:30.311Z", "disclosures": [] } }, diff --git a/metadata/modules/kargoBidAdapter.json b/metadata/modules/kargoBidAdapter.json index 003b44fc601..dc67cb78e6d 100644 --- a/metadata/modules/kargoBidAdapter.json +++ b/metadata/modules/kargoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://storage.cloud.kargo.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:24.538Z", + "timestamp": "2025-11-19T20:51:30.882Z", "disclosures": [ { "identifier": "krg_crb", diff --git a/metadata/modules/kueezRtbBidAdapter.json b/metadata/modules/kueezRtbBidAdapter.json index 6f425658948..7adb8674f49 100644 --- a/metadata/modules/kueezRtbBidAdapter.json +++ b/metadata/modules/kueezRtbBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://en.kueez.com/tcf.json": { - "timestamp": "2025-10-01T19:33:24.560Z", + "timestamp": "2025-11-19T20:51:30.901Z", "disclosures": [ { "identifier": "ck48wz12sqj7", diff --git a/metadata/modules/limelightDigitalBidAdapter.json b/metadata/modules/limelightDigitalBidAdapter.json index 192afc4cbaf..659ce95ef58 100644 --- a/metadata/modules/limelightDigitalBidAdapter.json +++ b/metadata/modules/limelightDigitalBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://policy.iion.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:24.635Z", + "timestamp": "2025-11-19T20:51:30.975Z", "disclosures": [] }, "https://orangeclickmedia.com/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:24.693Z", + "timestamp": "2025-11-19T20:51:31.006Z", "disclosures": [] } }, @@ -122,6 +122,13 @@ "aliasOf": "limelightDigital", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "vaayaMedia", + "aliasOf": "limelightDigital", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/liveIntentIdSystem.json b/metadata/modules/liveIntentIdSystem.json index fbd8bc52802..6239d4a31e8 100644 --- a/metadata/modules/liveIntentIdSystem.json +++ b/metadata/modules/liveIntentIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://b-code.liadm.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:24.694Z", + "timestamp": "2025-11-19T20:51:31.007Z", "disclosures": [ { "identifier": "_lc2_fpi", diff --git a/metadata/modules/liveIntentRtdProvider.json b/metadata/modules/liveIntentRtdProvider.json index aed5025d5d3..6ff7475fe19 100644 --- a/metadata/modules/liveIntentRtdProvider.json +++ b/metadata/modules/liveIntentRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://b-code.liadm.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:24.711Z", + "timestamp": "2025-11-19T20:51:31.046Z", "disclosures": [ { "identifier": "_lc2_fpi", diff --git a/metadata/modules/livewrappedBidAdapter.json b/metadata/modules/livewrappedBidAdapter.json index ade8658f6b2..00b970f6826 100644 --- a/metadata/modules/livewrappedBidAdapter.json +++ b/metadata/modules/livewrappedBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://content.lwadm.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:24.712Z", + "timestamp": "2025-11-19T20:51:31.047Z", "disclosures": [ { "identifier": "uid", diff --git a/metadata/modules/loopmeBidAdapter.json b/metadata/modules/loopmeBidAdapter.json index 78771b4e57e..219e66cd8b5 100644 --- a/metadata/modules/loopmeBidAdapter.json +++ b/metadata/modules/loopmeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://co.loopme.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:24.732Z", + "timestamp": "2025-11-19T20:51:31.069Z", "disclosures": [] } }, diff --git a/metadata/modules/lotamePanoramaIdSystem.json b/metadata/modules/lotamePanoramaIdSystem.json index 69f4567731b..46314251fc3 100644 --- a/metadata/modules/lotamePanoramaIdSystem.json +++ b/metadata/modules/lotamePanoramaIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tags.crwdcntrl.net/privacy/tcf-purposes.json": { - "timestamp": "2025-10-01T19:33:24.830Z", + "timestamp": "2025-11-19T20:51:31.214Z", "disclosures": [ { "identifier": "panoramaId", diff --git a/metadata/modules/luponmediaBidAdapter.json b/metadata/modules/luponmediaBidAdapter.json index fb7546b761a..797934db108 100644 --- a/metadata/modules/luponmediaBidAdapter.json +++ b/metadata/modules/luponmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://luponmedia.com/vendor_device_storage.json": { - "timestamp": "2025-10-01T19:33:24.846Z", + "timestamp": "2025-11-19T20:51:31.229Z", "disclosures": [] } }, diff --git a/metadata/modules/madvertiseBidAdapter.json b/metadata/modules/madvertiseBidAdapter.json index b2c6d4820f4..760afedd2cc 100644 --- a/metadata/modules/madvertiseBidAdapter.json +++ b/metadata/modules/madvertiseBidAdapter.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://mobile.mng-ads.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:25.251Z", + "https://adserver.bluestack.app/deviceStorage.json": { + "timestamp": "2025-11-19T20:51:31.756Z", "disclosures": [] } }, @@ -12,7 +12,7 @@ "componentName": "madvertise", "aliasOf": null, "gvlid": 153, - "disclosureURL": "https://mobile.mng-ads.com/deviceStorage.json" + "disclosureURL": "https://adserver.bluestack.app/deviceStorage.json" } ] } \ No newline at end of file diff --git a/metadata/modules/marsmediaBidAdapter.json b/metadata/modules/marsmediaBidAdapter.json index 2864b3bb7d1..616c4e5f5cf 100644 --- a/metadata/modules/marsmediaBidAdapter.json +++ b/metadata/modules/marsmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://mars.media/apis/tcf-v2.json": { - "timestamp": "2025-10-01T19:33:25.595Z", + "timestamp": "2025-11-19T20:51:32.115Z", "disclosures": [] } }, diff --git a/metadata/modules/mediaConsortiumBidAdapter.json b/metadata/modules/mediaConsortiumBidAdapter.json index 1c5c23edb26..21f9a7ad631 100644 --- a/metadata/modules/mediaConsortiumBidAdapter.json +++ b/metadata/modules/mediaConsortiumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.hubvisor.io/assets/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:25.713Z", + "timestamp": "2025-11-19T20:51:32.236Z", "disclosures": [ { "identifier": "hbv:turbo-cmp", diff --git a/metadata/modules/mediaforceBidAdapter.json b/metadata/modules/mediaforceBidAdapter.json index 82f97050bab..a5d933f8cc5 100644 --- a/metadata/modules/mediaforceBidAdapter.json +++ b/metadata/modules/mediaforceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://comparisons.org/privacy.json": { - "timestamp": "2025-10-01T19:33:25.876Z", + "timestamp": "2025-11-19T20:51:32.366Z", "disclosures": [] } }, diff --git a/metadata/modules/mediafuseBidAdapter.json b/metadata/modules/mediafuseBidAdapter.json index 05f5d283271..fbe6c0b1f36 100644 --- a/metadata/modules/mediafuseBidAdapter.json +++ b/metadata/modules/mediafuseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { - "timestamp": "2025-10-01T19:33:25.916Z", + "timestamp": "2025-11-19T20:51:32.385Z", "disclosures": [] } }, diff --git a/metadata/modules/mediagoBidAdapter.json b/metadata/modules/mediagoBidAdapter.json index 59cbb18c3f1..11856c9c3c2 100644 --- a/metadata/modules/mediagoBidAdapter.json +++ b/metadata/modules/mediagoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.mediago.io/js/tcf.json": { - "timestamp": "2025-10-01T19:33:25.916Z", + "timestamp": "2025-11-19T20:51:32.386Z", "disclosures": [] } }, diff --git a/metadata/modules/mediakeysBidAdapter.json b/metadata/modules/mediakeysBidAdapter.json index 7682cbe985b..c1866cdc1f0 100644 --- a/metadata/modules/mediakeysBidAdapter.json +++ b/metadata/modules/mediakeysBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s3.eu-west-3.amazonaws.com/adserving.resourcekeys.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:26.017Z", + "timestamp": "2025-11-19T20:51:32.459Z", "disclosures": [] } }, diff --git a/metadata/modules/medianetBidAdapter.json b/metadata/modules/medianetBidAdapter.json index c5e925bda60..4765a26023d 100644 --- a/metadata/modules/medianetBidAdapter.json +++ b/metadata/modules/medianetBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.media.net/tcfv2/gvl/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:26.305Z", + "timestamp": "2025-11-19T20:51:32.743Z", "disclosures": [ { "identifier": "_mNExInsl", @@ -246,7 +246,7 @@ ] }, "https://trustedstack.com/tcf/gvl/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:26.406Z", + "timestamp": "2025-11-19T20:51:32.879Z", "disclosures": [ { "identifier": "usp_status", diff --git a/metadata/modules/mediasquareBidAdapter.json b/metadata/modules/mediasquareBidAdapter.json index 97308e1deca..3ab9de062b7 100644 --- a/metadata/modules/mediasquareBidAdapter.json +++ b/metadata/modules/mediasquareBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://mediasquare.fr/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:26.446Z", + "timestamp": "2025-11-19T20:51:32.917Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidBidAdapter.json b/metadata/modules/mgidBidAdapter.json index 9eb3854b805..11ba1aedc46 100644 --- a/metadata/modules/mgidBidAdapter.json +++ b/metadata/modules/mgidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-10-01T19:33:26.970Z", + "timestamp": "2025-11-19T20:51:33.449Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidRtdProvider.json b/metadata/modules/mgidRtdProvider.json index 6e317be6253..b627ea6172a 100644 --- a/metadata/modules/mgidRtdProvider.json +++ b/metadata/modules/mgidRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-10-01T19:33:27.014Z", + "timestamp": "2025-11-19T20:51:33.545Z", "disclosures": [] } }, diff --git a/metadata/modules/mgidXBidAdapter.json b/metadata/modules/mgidXBidAdapter.json index 7f5379ef1c1..729e83d291c 100644 --- a/metadata/modules/mgidXBidAdapter.json +++ b/metadata/modules/mgidXBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.mgid.com/assets/devicestorage.json": { - "timestamp": "2025-10-01T19:33:27.014Z", + "timestamp": "2025-11-19T20:51:33.545Z", "disclosures": [] } }, diff --git a/metadata/modules/minutemediaBidAdapter.json b/metadata/modules/minutemediaBidAdapter.json index f5efdb8e295..98998cf1ad3 100644 --- a/metadata/modules/minutemediaBidAdapter.json +++ b/metadata/modules/minutemediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://disclosures.mmctsvc.com/device-storage.json": { - "timestamp": "2025-10-01T19:33:27.015Z", + "timestamp": "2025-11-19T20:51:33.546Z", "disclosures": [] } }, diff --git a/metadata/modules/missenaBidAdapter.json b/metadata/modules/missenaBidAdapter.json index 9cdc4964af5..4d433c3a2bf 100644 --- a/metadata/modules/missenaBidAdapter.json +++ b/metadata/modules/missenaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.missena.io/iab.json": { - "timestamp": "2025-10-01T19:33:27.047Z", + "timestamp": "2025-11-19T20:51:33.577Z", "disclosures": [] } }, diff --git a/metadata/modules/mobianRtdProvider.json b/metadata/modules/mobianRtdProvider.json index b88e06466d0..03233c61c19 100644 --- a/metadata/modules/mobianRtdProvider.json +++ b/metadata/modules/mobianRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://js.outcomes.net/tcf.json": { - "timestamp": "2025-10-01T19:33:27.101Z", + "timestamp": "2025-11-19T20:51:33.631Z", "disclosures": [] } }, diff --git a/metadata/modules/mobkoiBidAdapter.json b/metadata/modules/mobkoiBidAdapter.json index 18ce6a913d3..73706e77eb1 100644 --- a/metadata/modules/mobkoiBidAdapter.json +++ b/metadata/modules/mobkoiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.maximus.mobkoi.com/tcf/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:27.121Z", + "timestamp": "2025-11-19T20:51:33.651Z", "disclosures": [] } }, diff --git a/metadata/modules/mobkoiIdSystem.json b/metadata/modules/mobkoiIdSystem.json index 185a7321aab..3283be58b12 100644 --- a/metadata/modules/mobkoiIdSystem.json +++ b/metadata/modules/mobkoiIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.maximus.mobkoi.com/tcf/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:27.143Z", + "timestamp": "2025-11-19T20:51:33.671Z", "disclosures": [] } }, diff --git a/metadata/modules/msftBidAdapter.json b/metadata/modules/msftBidAdapter.json new file mode 100644 index 00000000000..083a53a9935 --- /dev/null +++ b/metadata/modules/msftBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json": { + "timestamp": "2025-11-19T20:51:33.671Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "msft", + "aliasOf": null, + "gvlid": 32, + "disclosureURL": "https://acdn.adnxs.com/gvl/1d/xandrdevicestoragedisclosures.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/nativeryBidAdapter.json b/metadata/modules/nativeryBidAdapter.json index e85659e0975..e4f1a7a04ed 100644 --- a/metadata/modules/nativeryBidAdapter.json +++ b/metadata/modules/nativeryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdnimg.nativery.com/widget/js/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:27.144Z", + "timestamp": "2025-11-19T20:51:33.672Z", "disclosures": [] } }, diff --git a/metadata/modules/nativoBidAdapter.json b/metadata/modules/nativoBidAdapter.json index 90589261709..36f38526ff4 100644 --- a/metadata/modules/nativoBidAdapter.json +++ b/metadata/modules/nativoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab.nativo.com/tcf-disclosures.json": { - "timestamp": "2025-10-01T19:33:27.598Z", + "timestamp": "2025-11-19T20:51:34.024Z", "disclosures": [] } }, diff --git a/metadata/modules/newspassidBidAdapter.json b/metadata/modules/newspassidBidAdapter.json index e9970b50611..1d072cda0f9 100644 --- a/metadata/modules/newspassidBidAdapter.json +++ b/metadata/modules/newspassidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.aditude.com/storageaccess.json": { - "timestamp": "2025-10-01T19:33:27.670Z", + "timestamp": "2025-11-19T20:51:34.046Z", "disclosures": [] } }, diff --git a/metadata/modules/nextMillenniumBidAdapter.json b/metadata/modules/nextMillenniumBidAdapter.json index 1befd02bda2..f647c5dee6b 100644 --- a/metadata/modules/nextMillenniumBidAdapter.json +++ b/metadata/modules/nextMillenniumBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://nextmillennium.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:27.670Z", + "timestamp": "2025-11-19T20:51:34.047Z", "disclosures": [] } }, diff --git a/metadata/modules/nextrollBidAdapter.json b/metadata/modules/nextrollBidAdapter.json index 30327afd741..e6a4502aa76 100644 --- a/metadata/modules/nextrollBidAdapter.json +++ b/metadata/modules/nextrollBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.adroll.com/shares/device_storage.json": { - "timestamp": "2025-10-01T19:33:27.712Z", + "timestamp": "2025-11-19T20:51:34.118Z", "disclosures": [ { "identifier": "__adroll_fpc", @@ -24,7 +24,7 @@ { "identifier": "__adroll_bounced3", "type": "cookie", - "maxAgeSeconds": 157680000, + "maxAgeSeconds": 7776000, "cookieRefresh": true, "purposes": [ 1, @@ -41,7 +41,7 @@ { "identifier": "__adroll_bounce_closed", "type": "cookie", - "maxAgeSeconds": 157680000, + "maxAgeSeconds": 34128000, "cookieRefresh": true, "purposes": [ 1, diff --git a/metadata/modules/nexx360BidAdapter.json b/metadata/modules/nexx360BidAdapter.json index 14b95e8865c..902e839f5cd 100644 --- a/metadata/modules/nexx360BidAdapter.json +++ b/metadata/modules/nexx360BidAdapter.json @@ -2,19 +2,19 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://fast.nexx360.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:27.927Z", + "timestamp": "2025-11-19T20:51:34.571Z", "disclosures": [] }, "https://static.first-id.fr/tcf/cookie.json": { - "timestamp": "2025-10-01T19:33:27.784Z", + "timestamp": "2025-11-19T20:51:34.414Z", "disclosures": [] }, "https://i.plug.it/banners/js/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:27.804Z", + "timestamp": "2025-11-19T20:51:34.441Z", "disclosures": [] }, "https://player.glomex.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:27.927Z", + "timestamp": "2025-11-19T20:51:34.571Z", "disclosures": [ { "identifier": "glomexUser", @@ -46,11 +46,11 @@ ] }, "https://mediafuse.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:27.927Z", + "timestamp": "2025-11-19T20:51:34.571Z", "disclosures": [] }, "https://gdpr.pubx.ai/devicestoragedisclosure.json": { - "timestamp": "2025-10-01T19:33:27.984Z", + "timestamp": "2025-11-19T20:51:34.657Z", "disclosures": [ { "identifier": "pubx:defaults", @@ -63,6 +63,10 @@ ] } ] + }, + "https://yieldbird.com/devicestorage.json": { + "timestamp": "2025-11-19T20:51:34.700Z", + "disclosures": [] } }, "components": [ @@ -184,6 +188,13 @@ "aliasOf": "nexx360", "gvlid": 1485, "disclosureURL": "https://gdpr.pubx.ai/devicestoragedisclosure.json" + }, + { + "componentType": "bidder", + "componentName": "ybidder", + "aliasOf": "nexx360", + "gvlid": 1253, + "disclosureURL": "https://yieldbird.com/devicestorage.json" } ] } \ No newline at end of file diff --git a/metadata/modules/nobidBidAdapter.json b/metadata/modules/nobidBidAdapter.json index ddfe66e050b..860d8b6a786 100644 --- a/metadata/modules/nobidBidAdapter.json +++ b/metadata/modules/nobidBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://public.servenobid.com/gdpr_tcf/vendor_device_storage_operational_disclosures.json": { - "timestamp": "2025-10-01T19:33:28.009Z", + "timestamp": "2025-11-19T20:51:35.069Z", "disclosures": [] } }, diff --git a/metadata/modules/nodalsAiRtdProvider.json b/metadata/modules/nodalsAiRtdProvider.json index d4ad367651f..80e71b663ae 100644 --- a/metadata/modules/nodalsAiRtdProvider.json +++ b/metadata/modules/nodalsAiRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.nodals.ai/vendor.json": { - "timestamp": "2025-10-01T19:33:28.023Z", + "timestamp": "2025-11-19T20:51:35.089Z", "disclosures": [ { "identifier": "localStorage", diff --git a/metadata/modules/novatiqIdSystem.json b/metadata/modules/novatiqIdSystem.json index 6b6c7dd7a3c..091a3406069 100644 --- a/metadata/modules/novatiqIdSystem.json +++ b/metadata/modules/novatiqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://novatiq.com/privacy/iab/novatiq.json": { - "timestamp": "2025-10-01T19:33:29.181Z", + "timestamp": "2025-11-19T20:51:36.315Z", "disclosures": [ { "identifier": "novatiq", diff --git a/metadata/modules/nubaBidAdapter.json b/metadata/modules/nubaBidAdapter.json new file mode 100644 index 00000000000..1ee19306146 --- /dev/null +++ b/metadata/modules/nubaBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "nuba", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/oguryBidAdapter.json b/metadata/modules/oguryBidAdapter.json index 0fada7a1b8f..cf40ea10f04 100644 --- a/metadata/modules/oguryBidAdapter.json +++ b/metadata/modules/oguryBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://privacy.ogury.co/disclosure.json": { - "timestamp": "2025-10-01T19:33:29.504Z", + "timestamp": "2025-11-19T20:51:36.644Z", "disclosures": [] } }, diff --git a/metadata/modules/omnidexBidAdapter.json b/metadata/modules/omnidexBidAdapter.json index 3b2d7e1e67a..9a67d3c863d 100644 --- a/metadata/modules/omnidexBidAdapter.json +++ b/metadata/modules/omnidexBidAdapter.json @@ -1,13 +1,89 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": {}, + "disclosures": { + "https://www.omni-dex.io/devicestorage.json": { + "timestamp": "2025-11-19T20:51:36.709Z", + "disclosures": [ + { + "identifier": "ck48wz12sqj7", + "type": "cookie", + "maxAgeSeconds": 31536000, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 10 + ] + }, + { + "identifier": "bah383vlj1", + "type": "cookie", + "maxAgeSeconds": 31536000, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 10 + ] + }, + { + "identifier": "vdzj1_{id}", + "type": "cookie", + "maxAgeSeconds": 31536000, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 10 + ] + }, + { + "identifier": "vdzh5_{id}", + "type": "cookie", + "maxAgeSeconds": 31536000, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 10 + ] + }, + { + "identifier": "vdzsync", + "type": "cookie", + "maxAgeSeconds": 31536000, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 10 + ] + } + ] + } + }, "components": [ { "componentType": "bidder", "componentName": "omnidex", "aliasOf": null, - "gvlid": null, - "disclosureURL": null + "gvlid": 1463, + "disclosureURL": "https://www.omni-dex.io/devicestorage.json" } ] } \ No newline at end of file diff --git a/metadata/modules/omsBidAdapter.json b/metadata/modules/omsBidAdapter.json index 866ece72c78..e802bd7f5fd 100644 --- a/metadata/modules/omsBidAdapter.json +++ b/metadata/modules/omsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.marphezis.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-10-01T19:33:29.580Z", + "timestamp": "2025-11-19T20:51:36.804Z", "disclosures": [] } }, diff --git a/metadata/modules/onetagBidAdapter.json b/metadata/modules/onetagBidAdapter.json index 5b847bccd80..8db87fd9a54 100644 --- a/metadata/modules/onetagBidAdapter.json +++ b/metadata/modules/onetagBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://onetag-cdn.com/privacy/tcf_storage.json": { - "timestamp": "2025-10-01T19:33:29.585Z", + "timestamp": "2025-11-19T20:51:36.805Z", "disclosures": [ { "identifier": "onetag_sid", diff --git a/metadata/modules/openwebBidAdapter.json b/metadata/modules/openwebBidAdapter.json index 5a6f8672895..950b15d5a40 100644 --- a/metadata/modules/openwebBidAdapter.json +++ b/metadata/modules/openwebBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spotim-prd-static-assets.s3.amazonaws.com/iab/device-storage.json": { - "timestamp": "2025-10-01T19:33:29.900Z", + "timestamp": "2025-11-19T20:51:37.157Z", "disclosures": [] } }, diff --git a/metadata/modules/openxBidAdapter.json b/metadata/modules/openxBidAdapter.json index f9c97a8cc5c..bbaade2d23e 100644 --- a/metadata/modules/openxBidAdapter.json +++ b/metadata/modules/openxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.openx.com/device-storage.json": { - "timestamp": "2025-10-01T19:33:29.931Z", + "timestamp": "2025-11-19T20:51:37.191Z", "disclosures": [] } }, diff --git a/metadata/modules/operaadsBidAdapter.json b/metadata/modules/operaadsBidAdapter.json index 15e42fa165f..311a6d714cb 100644 --- a/metadata/modules/operaadsBidAdapter.json +++ b/metadata/modules/operaadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://res.adx.opera.com/dsd.json": { - "timestamp": "2025-10-01T19:33:30.108Z", + "timestamp": "2025-11-19T20:51:37.267Z", "disclosures": [] } }, diff --git a/metadata/modules/optidigitalBidAdapter.json b/metadata/modules/optidigitalBidAdapter.json index 2719f0fadfc..7258949c64c 100644 --- a/metadata/modules/optidigitalBidAdapter.json +++ b/metadata/modules/optidigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://scripts.opti-digital.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:30.129Z", + "timestamp": "2025-11-19T20:51:37.295Z", "disclosures": [] } }, diff --git a/metadata/modules/optoutBidAdapter.json b/metadata/modules/optoutBidAdapter.json index 82242c64b8e..eed3e4d239c 100644 --- a/metadata/modules/optoutBidAdapter.json +++ b/metadata/modules/optoutBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://adserving.optoutadvertising.com/dsd": { - "timestamp": "2025-10-01T19:33:30.161Z", + "timestamp": "2025-11-19T20:51:37.340Z", "disclosures": [] } }, diff --git a/metadata/modules/orbidderBidAdapter.json b/metadata/modules/orbidderBidAdapter.json index aaedf04d0d3..b9df0f08646 100644 --- a/metadata/modules/orbidderBidAdapter.json +++ b/metadata/modules/orbidderBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://orbidder.otto.de/disclosure/dsd.json": { - "timestamp": "2025-10-01T19:33:30.416Z", + "timestamp": "2025-11-19T20:51:37.600Z", "disclosures": [] } }, diff --git a/metadata/modules/outbrainBidAdapter.json b/metadata/modules/outbrainBidAdapter.json index 57de2a88019..9f2cba4f046 100644 --- a/metadata/modules/outbrainBidAdapter.json +++ b/metadata/modules/outbrainBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.outbrain.com/privacy/wp-json/privacy/v2/devicestorage.json": { - "timestamp": "2025-10-01T19:33:30.742Z", + "timestamp": "2025-11-19T20:51:37.889Z", "disclosures": [ { "identifier": "dicbo_id", diff --git a/metadata/modules/ozoneBidAdapter.json b/metadata/modules/ozoneBidAdapter.json index 5d6fb8b373d..e5de08e8dfd 100644 --- a/metadata/modules/ozoneBidAdapter.json +++ b/metadata/modules/ozoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://prebid.the-ozone-project.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:31.018Z", + "timestamp": "2025-11-19T20:51:38.077Z", "disclosures": [] } }, diff --git a/metadata/modules/pairIdSystem.json b/metadata/modules/pairIdSystem.json index 003f43e1946..fbfb0d29fc8 100644 --- a/metadata/modules/pairIdSystem.json +++ b/metadata/modules/pairIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.gstatic.com/iabtcf/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:31.184Z", + "timestamp": "2025-11-19T20:51:38.338Z", "disclosures": [ { "identifier": "__gads", diff --git a/metadata/modules/performaxBidAdapter.json b/metadata/modules/performaxBidAdapter.json index fdb981b73a9..4a45428b467 100644 --- a/metadata/modules/performaxBidAdapter.json +++ b/metadata/modules/performaxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.performax.cz/device_storage.json": { - "timestamp": "2025-10-01T19:33:31.213Z", + "timestamp": "2025-11-19T20:51:38.364Z", "disclosures": [ { "identifier": "px2uid", diff --git a/metadata/modules/permutiveIdentityManagerIdSystem.json b/metadata/modules/permutiveIdentityManagerIdSystem.json index e8fc0cd1fac..e3b82502b76 100644 --- a/metadata/modules/permutiveIdentityManagerIdSystem.json +++ b/metadata/modules/permutiveIdentityManagerIdSystem.json @@ -1,12 +1,285 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": {}, + "disclosures": { + "https://assets.permutive.app/tcf/tcf.json": { + "timestamp": "2025-11-19T20:51:38.784Z", + "disclosures": [ + { + "identifier": "_pdfps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-models", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-queries", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prubicons", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psegs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pnativo", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-pvc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-enrichers", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-session", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-misc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psmart", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_paols", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrdbs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrprs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdem-state", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pfwqp", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_ppam", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-events-cache", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-loaded", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-tpd", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-consent", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "__permutiveConfigQueryParams", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "events_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "keys_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-prebid-*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdfps", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + } + ] + } + }, "components": [ { "componentType": "userId", "componentName": "permutiveIdentityManagerId", - "gvlid": null, - "disclosureURL": null, + "gvlid": 361, + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json", "aliasOf": null } ] diff --git a/metadata/modules/permutiveRtdProvider.json b/metadata/modules/permutiveRtdProvider.json index 0e675450fa8..14fedd6d831 100644 --- a/metadata/modules/permutiveRtdProvider.json +++ b/metadata/modules/permutiveRtdProvider.json @@ -1,12 +1,285 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": {}, + "disclosures": { + "https://assets.permutive.app/tcf/tcf.json": { + "timestamp": "2025-11-19T20:51:39.019Z", + "disclosures": [ + { + "identifier": "_pdfps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-models", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-queries", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prubicons", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psegs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pnativo", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-pvc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-enrichers", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-session", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-misc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_psmart", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_paols", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrdbs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pcrprs", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdem-state", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pfwqp", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_ppam", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_prps", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-events-cache", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-loaded", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pclmc", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-data-tpd", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-consent", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "__permutiveConfigQueryParams", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "events_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "keys_*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-prebid-*", + "type": "web", + "maxAgeSeconds": null, + "purposes": [ + 1 + ] + }, + { + "identifier": "permutive-id", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_papns", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + }, + { + "identifier": "_pdfps", + "type": "cookie", + "maxAgeSeconds": 15770000, + "cookieRefresh": false, + "purposes": [ + 1 + ] + } + ] + } + }, "components": [ { "componentType": "rtd", "componentName": "permutive", - "gvlid": null, - "disclosureURL": null + "gvlid": 361, + "disclosureURL": "https://assets.permutive.app/tcf/tcf.json" } ] } \ No newline at end of file diff --git a/metadata/modules/pgamsspBidAdapter.json b/metadata/modules/pgamsspBidAdapter.json index fc0d34bf660..29237763d3b 100644 --- a/metadata/modules/pgamsspBidAdapter.json +++ b/metadata/modules/pgamsspBidAdapter.json @@ -1,18 +1,13 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", - "disclosures": { - "https://pgammedia.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:31.623Z", - "disclosures": [] - } - }, + "disclosures": {}, "components": [ { "componentType": "bidder", "componentName": "pgamssp", "aliasOf": null, - "gvlid": 1353, - "disclosureURL": "https://pgammedia.com/devicestorage.json" + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/pixfutureBidAdapter.json b/metadata/modules/pixfutureBidAdapter.json index f6ee2df282f..b37cd00a26b 100644 --- a/metadata/modules/pixfutureBidAdapter.json +++ b/metadata/modules/pixfutureBidAdapter.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://pixfuture.com/vendor-disclosures.json": { - "timestamp": "2025-10-01T19:33:31.660Z", + "https://www.pixfuture.com/vendor-disclosures.json": { + "timestamp": "2025-11-19T20:51:39.021Z", "disclosures": [] } }, @@ -12,7 +12,7 @@ "componentName": "pixfuture", "aliasOf": null, "gvlid": 839, - "disclosureURL": "https://pixfuture.com/vendor-disclosures.json" + "disclosureURL": "https://www.pixfuture.com/vendor-disclosures.json" } ] } \ No newline at end of file diff --git a/metadata/modules/playdigoBidAdapter.json b/metadata/modules/playdigoBidAdapter.json index 9d31b1f295c..60381299bfe 100644 --- a/metadata/modules/playdigoBidAdapter.json +++ b/metadata/modules/playdigoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://playdigo.com/file.json": { - "timestamp": "2025-10-01T19:33:31.706Z", + "timestamp": "2025-11-19T20:51:39.074Z", "disclosures": [] } }, diff --git a/metadata/modules/prebid-core.json b/metadata/modules/prebid-core.json index 6923cf80109..7fbdf54ee46 100644 --- a/metadata/modules/prebid-core.json +++ b/metadata/modules/prebid-core.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/probes.json": { - "timestamp": "2025-10-01T19:33:04.693Z", + "timestamp": "2025-11-19T20:51:08.091Z", "disclosures": [ { "identifier": "_rdc*", @@ -23,7 +23,7 @@ ] }, "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/debugging.json": { - "timestamp": "2025-10-01T19:33:04.695Z", + "timestamp": "2025-11-19T20:51:08.092Z", "disclosures": [ { "identifier": "__*_debugging__", diff --git a/metadata/modules/precisoBidAdapter.json b/metadata/modules/precisoBidAdapter.json index 9e3a1b27047..e05b75bd550 100644 --- a/metadata/modules/precisoBidAdapter.json +++ b/metadata/modules/precisoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://preciso.net/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:31.886Z", + "timestamp": "2025-11-19T20:51:39.262Z", "disclosures": [ { "identifier": "XXXXX_viewnew", diff --git a/metadata/modules/prismaBidAdapter.json b/metadata/modules/prismaBidAdapter.json index f238c210029..b9873fe45c5 100644 --- a/metadata/modules/prismaBidAdapter.json +++ b/metadata/modules/prismaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://fast.nexx360.io/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:32.117Z", + "timestamp": "2025-11-19T20:51:39.489Z", "disclosures": [] } }, diff --git a/metadata/modules/programmaticXBidAdapter.json b/metadata/modules/programmaticXBidAdapter.json index 0034179edd1..fcbe8780460 100644 --- a/metadata/modules/programmaticXBidAdapter.json +++ b/metadata/modules/programmaticXBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://progrtb.com/tcf-vendor-disclosures.json": { - "timestamp": "2025-10-01T19:33:32.117Z", + "timestamp": "2025-11-19T20:51:39.490Z", "disclosures": [] } }, diff --git a/metadata/modules/proxistoreBidAdapter.json b/metadata/modules/proxistoreBidAdapter.json index ef9965a17a1..1744e2edd89 100644 --- a/metadata/modules/proxistoreBidAdapter.json +++ b/metadata/modules/proxistoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://abs.proxistore.com/assets/json/proxistore_device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:32.173Z", + "timestamp": "2025-11-19T20:51:39.534Z", "disclosures": [] } }, diff --git a/metadata/modules/publicGoodBidAdapter.json b/metadata/modules/publicGoodBidAdapter.json new file mode 100644 index 00000000000..a492629c705 --- /dev/null +++ b/metadata/modules/publicGoodBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "publicgood", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/publinkIdSystem.json b/metadata/modules/publinkIdSystem.json index 215f8dd933e..4c7748afbf2 100644 --- a/metadata/modules/publinkIdSystem.json +++ b/metadata/modules/publinkIdSystem.json @@ -1,8 +1,8 @@ { "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { - "https://s-usweb.dotomi.com/assets/js/taggy-js/2.17.0/device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:32.639Z", + "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.8/device_storage_disclosure.json": { + "timestamp": "2025-11-19T20:51:39.911Z", "disclosures": [ { "identifier": "dtm_status", @@ -441,6 +441,44 @@ 11 ] }, + { + "identifier": "_pubcid_exp", + "type": "web", + "maxAgeSeconds": null, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, + { + "identifier": "__dtmtest_*", + "type": "cookie", + "maxAgeSeconds": 60, + "cookieRefresh": false, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] + }, { "identifier": "_rl_aud", "type": "cookie", @@ -516,6 +554,25 @@ 10, 11 ] + }, + { + "identifier": "hConversionEventId", + "type": "cookie", + "maxAgeSeconds": 2592000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 + ] } ] } @@ -525,7 +582,7 @@ "componentType": "userId", "componentName": "publinkId", "gvlid": 24, - "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.17.0/device_storage_disclosure.json", + "disclosureURL": "https://s-usweb.dotomi.com/assets/js/taggy-js/2.18.8/device_storage_disclosure.json", "aliasOf": null } ] diff --git a/metadata/modules/pubmaticBidAdapter.json b/metadata/modules/pubmaticBidAdapter.json index d2cffb5f45a..2884bce73ef 100644 --- a/metadata/modules/pubmaticBidAdapter.json +++ b/metadata/modules/pubmaticBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.pubmatic.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:32.642Z", + "timestamp": "2025-11-19T20:51:39.912Z", "disclosures": [] } }, diff --git a/metadata/modules/pubmaticIdSystem.json b/metadata/modules/pubmaticIdSystem.json index c9883a28965..7883f89271a 100644 --- a/metadata/modules/pubmaticIdSystem.json +++ b/metadata/modules/pubmaticIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.pubmatic.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:32.660Z", + "timestamp": "2025-11-19T20:51:39.955Z", "disclosures": [] } }, diff --git a/metadata/modules/pulsepointBidAdapter.json b/metadata/modules/pulsepointBidAdapter.json index af3014c97ef..0fcc3d50066 100644 --- a/metadata/modules/pulsepointBidAdapter.json +++ b/metadata/modules/pulsepointBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bh.contextweb.com/tcf/vendorInfo.json": { - "timestamp": "2025-10-01T19:33:32.665Z", + "timestamp": "2025-11-19T20:51:39.957Z", "disclosures": [] } }, diff --git a/metadata/modules/quantcastBidAdapter.json b/metadata/modules/quantcastBidAdapter.json index ab9a07e1231..52da89d65fe 100644 --- a/metadata/modules/quantcastBidAdapter.json +++ b/metadata/modules/quantcastBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.quantcast.com/.well-known/devicestorage.json": { - "timestamp": "2025-10-01T19:33:32.685Z", + "timestamp": "2025-11-19T20:51:39.976Z", "disclosures": [ { "identifier": "__qca", diff --git a/metadata/modules/quantcastIdSystem.json b/metadata/modules/quantcastIdSystem.json index de35fa6cd5a..af2c2e18ed2 100644 --- a/metadata/modules/quantcastIdSystem.json +++ b/metadata/modules/quantcastIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.quantcast.com/.well-known/devicestorage.json": { - "timestamp": "2025-10-01T19:33:32.891Z", + "timestamp": "2025-11-19T20:51:40.184Z", "disclosures": [ { "identifier": "__qca", diff --git a/metadata/modules/r2b2BidAdapter.json b/metadata/modules/r2b2BidAdapter.json index 5f3a45eb197..d01e396cf85 100644 --- a/metadata/modules/r2b2BidAdapter.json +++ b/metadata/modules/r2b2BidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://delivery.r2b2.io/cookie_disclosure": { - "timestamp": "2025-10-01T19:33:32.894Z", + "timestamp": "2025-11-19T20:51:40.185Z", "disclosures": [ { "identifier": "AdTrack-hide-*", diff --git a/metadata/modules/readpeakBidAdapter.json b/metadata/modules/readpeakBidAdapter.json index 84e9c4b4ce0..84a63dfce45 100644 --- a/metadata/modules/readpeakBidAdapter.json +++ b/metadata/modules/readpeakBidAdapter.json @@ -2,8 +2,26 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static.readpeak.com/tcf/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:33.313Z", - "disclosures": [] + "timestamp": "2025-11-19T20:51:40.573Z", + "disclosures": [ + { + "identifier": "rp_uidfp", + "type": "cookie", + "maxAgeSeconds": 33696000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 10 + ] + } + ] } }, "components": [ diff --git a/metadata/modules/relayBidAdapter.json b/metadata/modules/relayBidAdapter.json index 7af96fec335..e7400919ba9 100644 --- a/metadata/modules/relayBidAdapter.json +++ b/metadata/modules/relayBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://relay42.com/hubfs/raw_assets/public/IAB.json": { - "timestamp": "2025-10-01T19:33:33.334Z", + "timestamp": "2025-11-19T20:51:40.603Z", "disclosures": [] } }, diff --git a/metadata/modules/relevantdigitalBidAdapter.json b/metadata/modules/relevantdigitalBidAdapter.json index 5c07f5a357a..c6676bc9a18 100644 --- a/metadata/modules/relevantdigitalBidAdapter.json +++ b/metadata/modules/relevantdigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.relevant-digital.com/resources/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:33.407Z", + "timestamp": "2025-11-19T20:51:40.699Z", "disclosures": [] } }, diff --git a/metadata/modules/resetdigitalBidAdapter.json b/metadata/modules/resetdigitalBidAdapter.json index 0bfbfd24ce6..320913ab3d3 100644 --- a/metadata/modules/resetdigitalBidAdapter.json +++ b/metadata/modules/resetdigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://resetdigital.co/GDPR-TCF.json": { - "timestamp": "2025-10-01T19:33:33.564Z", + "timestamp": "2025-11-19T20:51:40.878Z", "disclosures": [] } }, diff --git a/metadata/modules/responsiveAdsBidAdapter.json b/metadata/modules/responsiveAdsBidAdapter.json index b716f09dfac..9fa482f8c47 100644 --- a/metadata/modules/responsiveAdsBidAdapter.json +++ b/metadata/modules/responsiveAdsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://publish.responsiveads.com/tcf/tcf-v2.json": { - "timestamp": "2025-10-01T19:33:33.622Z", + "timestamp": "2025-11-19T20:51:40.922Z", "disclosures": [] } }, diff --git a/metadata/modules/revcontentBidAdapter.json b/metadata/modules/revcontentBidAdapter.json index 81bea2d60d5..c85e30b1da9 100644 --- a/metadata/modules/revcontentBidAdapter.json +++ b/metadata/modules/revcontentBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sothebys.revcontent.com/static/device_storage.json": { - "timestamp": "2025-10-01T19:33:33.647Z", + "timestamp": "2025-11-19T20:51:40.952Z", "disclosures": [ { "identifier": "__ID", diff --git a/metadata/modules/rhythmoneBidAdapter.json b/metadata/modules/rhythmoneBidAdapter.json index fef28620df6..8a309ace5cf 100644 --- a/metadata/modules/rhythmoneBidAdapter.json +++ b/metadata/modules/rhythmoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://video.unrulymedia.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:33.681Z", + "timestamp": "2025-11-19T20:51:40.980Z", "disclosures": [] } }, diff --git a/metadata/modules/richaudienceBidAdapter.json b/metadata/modules/richaudienceBidAdapter.json index 80796ccd8fb..4b9eafb1589 100644 --- a/metadata/modules/richaudienceBidAdapter.json +++ b/metadata/modules/richaudienceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdnj.richaudience.com/52a26ab9400b2a9f5aabfa20acf3196g.json": { - "timestamp": "2025-10-01T19:33:33.919Z", + "timestamp": "2025-11-19T20:51:41.216Z", "disclosures": [] } }, diff --git a/metadata/modules/riseBidAdapter.json b/metadata/modules/riseBidAdapter.json index 668f9f34a0a..e83029082bb 100644 --- a/metadata/modules/riseBidAdapter.json +++ b/metadata/modules/riseBidAdapter.json @@ -2,11 +2,11 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://d2pm7iglz0b6eq.cloudfront.net/RiseDeviceStorage.json": { - "timestamp": "2025-10-01T19:33:33.999Z", + "timestamp": "2025-11-19T20:51:41.284Z", "disclosures": [] }, "https://spotim-prd-static-assets.s3.amazonaws.com/iab/device-storage.json": { - "timestamp": "2025-10-01T19:33:33.999Z", + "timestamp": "2025-11-19T20:51:41.284Z", "disclosures": [] } }, diff --git a/metadata/modules/rixengineBidAdapter.json b/metadata/modules/rixengineBidAdapter.json index 7c73250bb29..db0ff171189 100644 --- a/metadata/modules/rixengineBidAdapter.json +++ b/metadata/modules/rixengineBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.algorix.co/gdpr-disclosure.json": { - "timestamp": "2025-10-01T19:33:34.000Z", + "timestamp": "2025-11-19T20:51:41.285Z", "disclosures": [] } }, diff --git a/metadata/modules/rtbhouseBidAdapter.json b/metadata/modules/rtbhouseBidAdapter.json index 9fbe35073f4..4af03548531 100644 --- a/metadata/modules/rtbhouseBidAdapter.json +++ b/metadata/modules/rtbhouseBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://rtbhouse.com/DeviceStorage.json": { - "timestamp": "2025-10-01T19:33:34.050Z", + "timestamp": "2025-11-19T20:51:41.308Z", "disclosures": [ { "identifier": "_rtbh.*", diff --git a/metadata/modules/rubiconBidAdapter.json b/metadata/modules/rubiconBidAdapter.json index d279ac6cd9f..4c0a7bea043 100644 --- a/metadata/modules/rubiconBidAdapter.json +++ b/metadata/modules/rubiconBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gdpr.rubiconproject.com/dvplus/devicestoragedisclosure.json": { - "timestamp": "2025-10-01T19:33:34.197Z", + "timestamp": "2025-11-19T20:51:41.772Z", "disclosures": [] } }, diff --git a/metadata/modules/scaliburBidAdapter.json b/metadata/modules/scaliburBidAdapter.json new file mode 100644 index 00000000000..435a1614543 --- /dev/null +++ b/metadata/modules/scaliburBidAdapter.json @@ -0,0 +1,35 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://legal.overwolf.com/docs/overwolf/website/deviceStorageDisclosure.json": { + "timestamp": "2025-11-19T20:51:42.056Z", + "disclosures": [ + { + "identifier": "scluid", + "type": "cookie", + "maxAgeSeconds": 157680000, + "cookieRefresh": true, + "purposes": [ + 1, + 2, + 3, + 4, + 7, + 8, + 9, + 10 + ] + } + ] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "scalibur", + "aliasOf": null, + "gvlid": 1471, + "disclosureURL": "https://legal.overwolf.com/docs/overwolf/website/deviceStorageDisclosure.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/screencoreBidAdapter.json b/metadata/modules/screencoreBidAdapter.json index a91d9fd2a3f..534b6c896fc 100644 --- a/metadata/modules/screencoreBidAdapter.json +++ b/metadata/modules/screencoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://screencore.io/tcf.json": { - "timestamp": "2025-10-01T19:33:34.466Z", + "timestamp": "2025-11-19T20:51:42.072Z", "disclosures": null } }, diff --git a/metadata/modules/seedingAllianceBidAdapter.json b/metadata/modules/seedingAllianceBidAdapter.json index 985b195d4fe..fd098334269 100644 --- a/metadata/modules/seedingAllianceBidAdapter.json +++ b/metadata/modules/seedingAllianceBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s.nativendo.de/cdn/asset/tcf/purpose-specific-storage-and-access-information.json": { - "timestamp": "2025-10-01T19:33:37.101Z", + "timestamp": "2025-11-19T20:51:44.670Z", "disclosures": [] } }, diff --git a/metadata/modules/seedtagBidAdapter.json b/metadata/modules/seedtagBidAdapter.json index 3f4a56b89ba..af146802a6b 100644 --- a/metadata/modules/seedtagBidAdapter.json +++ b/metadata/modules/seedtagBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.seedtag.com/vendor.json": { - "timestamp": "2025-10-01T19:33:37.125Z", + "timestamp": "2025-11-19T20:51:44.716Z", "disclosures": [] } }, diff --git a/metadata/modules/semantiqRtdProvider.json b/metadata/modules/semantiqRtdProvider.json index 67824e48fc2..9f3260038dd 100644 --- a/metadata/modules/semantiqRtdProvider.json +++ b/metadata/modules/semantiqRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://audienzz.com/device_storage_disclosure_vendor_783.json": { - "timestamp": "2025-10-01T19:33:37.126Z", + "timestamp": "2025-11-19T20:51:44.725Z", "disclosures": [] } }, diff --git a/metadata/modules/setupadBidAdapter.json b/metadata/modules/setupadBidAdapter.json index 37dc16b83f7..64902e02316 100644 --- a/metadata/modules/setupadBidAdapter.json +++ b/metadata/modules/setupadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cookies.stpd.cloud/disclosures.json": { - "timestamp": "2025-10-01T19:33:37.184Z", + "timestamp": "2025-11-19T20:51:44.797Z", "disclosures": [] } }, diff --git a/metadata/modules/sevioBidAdapter.json b/metadata/modules/sevioBidAdapter.json index 65f3fd48752..37262a8bb45 100644 --- a/metadata/modules/sevioBidAdapter.json +++ b/metadata/modules/sevioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sevio.com/tcf.json": { - "timestamp": "2025-10-01T19:33:37.279Z", + "timestamp": "2025-11-19T20:51:44.964Z", "disclosures": [] } }, diff --git a/metadata/modules/sharedIdSystem.json b/metadata/modules/sharedIdSystem.json index fd8492170d3..103686eec7f 100644 --- a/metadata/modules/sharedIdSystem.json +++ b/metadata/modules/sharedIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/sharedId-optout.json": { - "timestamp": "2025-10-01T19:33:37.407Z", + "timestamp": "2025-11-19T20:51:45.132Z", "disclosures": [ { "identifier": "_pubcid_optout", diff --git a/metadata/modules/sharethroughBidAdapter.json b/metadata/modules/sharethroughBidAdapter.json index 8556e1474fb..10ae75097d0 100644 --- a/metadata/modules/sharethroughBidAdapter.json +++ b/metadata/modules/sharethroughBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://assets.sharethrough.com/gvl.json": { - "timestamp": "2025-10-01T19:33:37.407Z", + "timestamp": "2025-11-19T20:51:45.139Z", "disclosures": [] } }, diff --git a/metadata/modules/showheroes-bsBidAdapter.json b/metadata/modules/showheroes-bsBidAdapter.json index 9ae9effd1c6..a42de2663f7 100644 --- a/metadata/modules/showheroes-bsBidAdapter.json +++ b/metadata/modules/showheroes-bsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://static-origin.showheroes.com/gvl_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:37.435Z", + "timestamp": "2025-11-19T20:51:45.221Z", "disclosures": [] } }, diff --git a/metadata/modules/silvermobBidAdapter.json b/metadata/modules/silvermobBidAdapter.json index 4236fc37a38..d5a8d366403 100644 --- a/metadata/modules/silvermobBidAdapter.json +++ b/metadata/modules/silvermobBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://silvermob.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:37.908Z", + "timestamp": "2025-11-19T20:51:45.652Z", "disclosures": [] } }, diff --git a/metadata/modules/sirdataRtdProvider.json b/metadata/modules/sirdataRtdProvider.json index 59f69115d8b..a0000813db7 100644 --- a/metadata/modules/sirdataRtdProvider.json +++ b/metadata/modules/sirdataRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.sirdata.eu/sirdata_device_storage_disclosure.json": { - "timestamp": "2025-10-01T19:33:37.921Z", + "timestamp": "2025-11-19T20:51:45.668Z", "disclosures": [] } }, diff --git a/metadata/modules/smaatoBidAdapter.json b/metadata/modules/smaatoBidAdapter.json index 67decd2afc4..f78f52100c4 100644 --- a/metadata/modules/smaatoBidAdapter.json +++ b/metadata/modules/smaatoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://resources.smaato.com/hubfs/Smaato/IAB/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:38.230Z", + "timestamp": "2025-11-19T20:51:45.988Z", "disclosures": [] } }, diff --git a/metadata/modules/smartadserverBidAdapter.json b/metadata/modules/smartadserverBidAdapter.json index 34018d74a86..40f3a1a6e2d 100644 --- a/metadata/modules/smartadserverBidAdapter.json +++ b/metadata/modules/smartadserverBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://apps.smartadserver.com/device-storage-disclosures/equativDeviceStorageDisclosures.json": { - "timestamp": "2025-10-01T19:33:38.295Z", + "timestamp": "2025-11-19T20:51:46.084Z", "disclosures": [] } }, diff --git a/metadata/modules/smarthubBidAdapter.json b/metadata/modules/smarthubBidAdapter.json index 30d325335f5..eb77b60b1e7 100644 --- a/metadata/modules/smarthubBidAdapter.json +++ b/metadata/modules/smarthubBidAdapter.json @@ -78,6 +78,20 @@ "aliasOf": "smarthub", "gvlid": null, "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "amcom", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null + }, + { + "componentType": "bidder", + "componentName": "adastra", + "aliasOf": "smarthub", + "gvlid": null, + "disclosureURL": null } ] } \ No newline at end of file diff --git a/metadata/modules/smartxBidAdapter.json b/metadata/modules/smartxBidAdapter.json index 20cd2bf27be..fd7eabf9d7c 100644 --- a/metadata/modules/smartxBidAdapter.json +++ b/metadata/modules/smartxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.smartclip.net/iab/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:38.296Z", + "timestamp": "2025-11-19T20:51:46.085Z", "disclosures": [] } }, diff --git a/metadata/modules/smartyadsBidAdapter.json b/metadata/modules/smartyadsBidAdapter.json index 53c74c86c87..20ba7fece8e 100644 --- a/metadata/modules/smartyadsBidAdapter.json +++ b/metadata/modules/smartyadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://smartyads.com/tcf.json": { - "timestamp": "2025-10-01T19:33:38.314Z", + "timestamp": "2025-11-19T20:51:46.102Z", "disclosures": [] } }, diff --git a/metadata/modules/smilewantedBidAdapter.json b/metadata/modules/smilewantedBidAdapter.json index 35fe7ead8af..bba81b93490 100644 --- a/metadata/modules/smilewantedBidAdapter.json +++ b/metadata/modules/smilewantedBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://smilewanted.com/vendor-device-storage-disclosures.json": { - "timestamp": "2025-09-25T13:09:41.446Z", + "timestamp": "2025-11-19T20:51:46.145Z", "disclosures": [] } }, diff --git a/metadata/modules/snigelBidAdapter.json b/metadata/modules/snigelBidAdapter.json index 2f678db2792..13a8f3b522e 100644 --- a/metadata/modules/snigelBidAdapter.json +++ b/metadata/modules/snigelBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.snigelweb.com/gvl/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:38.793Z", + "timestamp": "2025-11-19T20:51:46.631Z", "disclosures": [] } }, diff --git a/metadata/modules/sonaradsBidAdapter.json b/metadata/modules/sonaradsBidAdapter.json index 4102b563b36..9972c24ccc8 100644 --- a/metadata/modules/sonaradsBidAdapter.json +++ b/metadata/modules/sonaradsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bridgeupp.com/device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:39.040Z", + "timestamp": "2025-11-19T20:51:46.676Z", "disclosures": [] } }, diff --git a/metadata/modules/sonobiBidAdapter.json b/metadata/modules/sonobiBidAdapter.json index 2f0570e4dae..45f0cfcd727 100644 --- a/metadata/modules/sonobiBidAdapter.json +++ b/metadata/modules/sonobiBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://sonobi.com/tcf2-device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:39.259Z", + "timestamp": "2025-11-19T20:51:46.917Z", "disclosures": [] } }, diff --git a/metadata/modules/sovrnBidAdapter.json b/metadata/modules/sovrnBidAdapter.json index 10a90829964..72d52ec296c 100644 --- a/metadata/modules/sovrnBidAdapter.json +++ b/metadata/modules/sovrnBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.sovrn.com/tcf-cookie-disclosure/disclosure.json": { - "timestamp": "2025-10-01T19:33:39.492Z", + "timestamp": "2025-11-19T20:51:47.152Z", "disclosures": [] } }, diff --git a/metadata/modules/sparteoBidAdapter.json b/metadata/modules/sparteoBidAdapter.json index a35cc3dd158..8775c8fd162 100644 --- a/metadata/modules/sparteoBidAdapter.json +++ b/metadata/modules/sparteoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.bricks-co.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:39.513Z", + "timestamp": "2025-11-19T20:51:47.212Z", "disclosures": [ { "identifier": "fastCMP-addtlConsent", diff --git a/metadata/modules/ssmasBidAdapter.json b/metadata/modules/ssmasBidAdapter.json index 9362fac709d..9987d6d468e 100644 --- a/metadata/modules/ssmasBidAdapter.json +++ b/metadata/modules/ssmasBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://semseoymas.com/iab.json": { - "timestamp": "2025-10-01T19:33:39.796Z", + "timestamp": "2025-11-19T20:51:47.488Z", "disclosures": null } }, diff --git a/metadata/modules/sspBCBidAdapter.json b/metadata/modules/sspBCBidAdapter.json index 3c4f8f9fc60..4b74ae80cf4 100644 --- a/metadata/modules/sspBCBidAdapter.json +++ b/metadata/modules/sspBCBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ssp.wp.pl/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:40.317Z", + "timestamp": "2025-11-19T20:51:48.083Z", "disclosures": null } }, diff --git a/metadata/modules/stackadaptBidAdapter.json b/metadata/modules/stackadaptBidAdapter.json index 93c1b07cda7..0c10acf9c32 100644 --- a/metadata/modules/stackadaptBidAdapter.json +++ b/metadata/modules/stackadaptBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://s3.amazonaws.com/stackadapt_public/disclosures.json": { - "timestamp": "2025-10-01T19:33:40.318Z", + "timestamp": "2025-11-19T20:51:48.083Z", "disclosures": [ { "identifier": "sa-camp-*", diff --git a/metadata/modules/startioBidAdapter.json b/metadata/modules/startioBidAdapter.json index 14af2f21407..345eef10b8e 100644 --- a/metadata/modules/startioBidAdapter.json +++ b/metadata/modules/startioBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://info.startappservice.com/tcf/start.io_domains.json": { - "timestamp": "2025-10-01T19:33:40.348Z", + "timestamp": "2025-11-19T20:51:48.112Z", "disclosures": [] } }, diff --git a/metadata/modules/stroeerCoreBidAdapter.json b/metadata/modules/stroeerCoreBidAdapter.json index 6cf322d79ea..8979658efb2 100644 --- a/metadata/modules/stroeerCoreBidAdapter.json +++ b/metadata/modules/stroeerCoreBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.stroeer.de/StroeerSSP_deviceStorage.json": { - "timestamp": "2025-10-01T19:33:40.366Z", + "timestamp": "2025-11-19T20:51:48.129Z", "disclosures": [] } }, diff --git a/metadata/modules/stvBidAdapter.json b/metadata/modules/stvBidAdapter.json index 50422c517b5..431da5d0b5e 100644 --- a/metadata/modules/stvBidAdapter.json +++ b/metadata/modules/stvBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.adtech.app/gen/deviceStorageDisclosure/stv.json": { - "timestamp": "2025-10-01T19:33:40.761Z", + "timestamp": "2025-11-19T20:51:48.464Z", "disclosures": [] } }, diff --git a/metadata/modules/sublimeBidAdapter.json b/metadata/modules/sublimeBidAdapter.json index b417bb4a9cf..ed5e5e42678 100644 --- a/metadata/modules/sublimeBidAdapter.json +++ b/metadata/modules/sublimeBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://gdpr.ayads.co/cookiepolicy.json": { - "timestamp": "2025-10-01T19:33:41.465Z", + "timestamp": "2025-11-19T20:51:49.146Z", "disclosures": [ { "identifier": "dnt", diff --git a/metadata/modules/taboolaBidAdapter.json b/metadata/modules/taboolaBidAdapter.json index 8fe7f782559..fb580391c59 100644 --- a/metadata/modules/taboolaBidAdapter.json +++ b/metadata/modules/taboolaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://accessrequest.taboola.com/iab-tcf-v2-disclosure.json": { - "timestamp": "2025-10-01T19:33:41.494Z", + "timestamp": "2025-11-19T20:51:49.410Z", "disclosures": [ { "identifier": "trc_cookie_storage", @@ -240,17 +240,28 @@ { "identifier": "taboola:shopify:test", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1 + ] }, { "identifier": "taboola:shopify:enable_debug_logging", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 10 + ] }, { "identifier": "taboola:shopify:pixel_allow_checkout_start", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 3 + ] }, { "identifier": "taboola:shopify:page_view", diff --git a/metadata/modules/taboolaIdSystem.json b/metadata/modules/taboolaIdSystem.json index 6d460af6dec..0a528a2a855 100644 --- a/metadata/modules/taboolaIdSystem.json +++ b/metadata/modules/taboolaIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://accessrequest.taboola.com/iab-tcf-v2-disclosure.json": { - "timestamp": "2025-10-01T19:33:42.155Z", + "timestamp": "2025-11-19T20:51:50.104Z", "disclosures": [ { "identifier": "trc_cookie_storage", @@ -240,17 +240,28 @@ { "identifier": "taboola:shopify:test", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1 + ] }, { "identifier": "taboola:shopify:enable_debug_logging", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 10 + ] }, { "identifier": "taboola:shopify:pixel_allow_checkout_start", "type": "web", - "maxAgeSeconds": null + "maxAgeSeconds": null, + "purposes": [ + 1, + 3 + ] }, { "identifier": "taboola:shopify:page_view", diff --git a/metadata/modules/tadvertisingBidAdapter.json b/metadata/modules/tadvertisingBidAdapter.json index 3b3c5319697..c1eff5141fc 100644 --- a/metadata/modules/tadvertisingBidAdapter.json +++ b/metadata/modules/tadvertisingBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tcf.emetriq.de/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:42.155Z", + "timestamp": "2025-11-19T20:51:50.104Z", "disclosures": [] } }, diff --git a/metadata/modules/tappxBidAdapter.json b/metadata/modules/tappxBidAdapter.json index 157538ee2ba..0820c6e91d0 100644 --- a/metadata/modules/tappxBidAdapter.json +++ b/metadata/modules/tappxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://tappx.com/devicestorage.json": { - "timestamp": "2025-10-01T19:33:42.156Z", + "timestamp": "2025-11-19T20:51:50.105Z", "disclosures": [] } }, diff --git a/metadata/modules/targetVideoBidAdapter.json b/metadata/modules/targetVideoBidAdapter.json index 924c5dc7049..d928341fda6 100644 --- a/metadata/modules/targetVideoBidAdapter.json +++ b/metadata/modules/targetVideoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://target-video.com/vendors-device-storage-and-operational-disclosures.json": { - "timestamp": "2025-10-01T19:33:42.228Z", + "timestamp": "2025-11-19T20:51:50.138Z", "disclosures": [ { "identifier": "brid_location", diff --git a/metadata/modules/teadsBidAdapter.json b/metadata/modules/teadsBidAdapter.json index 020ec0102b7..0d1c9da97ec 100644 --- a/metadata/modules/teadsBidAdapter.json +++ b/metadata/modules/teadsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab-cookie-disclosure.teads.tv/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.228Z", + "timestamp": "2025-11-19T20:51:50.138Z", "disclosures": [] } }, diff --git a/metadata/modules/teadsIdSystem.json b/metadata/modules/teadsIdSystem.json index 73c3a340164..b05bfee171a 100644 --- a/metadata/modules/teadsIdSystem.json +++ b/metadata/modules/teadsIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://iab-cookie-disclosure.teads.tv/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.253Z", + "timestamp": "2025-11-19T20:51:50.164Z", "disclosures": [] } }, diff --git a/metadata/modules/tealBidAdapter.json b/metadata/modules/tealBidAdapter.json index 27b4f73957b..f3000f6cc1d 100644 --- a/metadata/modules/tealBidAdapter.json +++ b/metadata/modules/tealBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://c.bids.ws/iab/disclosures.json": { - "timestamp": "2025-10-01T19:33:42.253Z", + "timestamp": "2025-11-19T20:51:50.165Z", "disclosures": [] } }, diff --git a/metadata/modules/tncIdSystem.json b/metadata/modules/tncIdSystem.json index ec7f22ed42a..4159d72a123 100644 --- a/metadata/modules/tncIdSystem.json +++ b/metadata/modules/tncIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://js.tncid.app/iab-tcf-device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:42.289Z", + "timestamp": "2025-11-19T20:51:50.204Z", "disclosures": [] } }, diff --git a/metadata/modules/topicsFpdModule.json b/metadata/modules/topicsFpdModule.json index b33477c2099..3a1e57ff65b 100644 --- a/metadata/modules/topicsFpdModule.json +++ b/metadata/modules/topicsFpdModule.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/topicsFpdModule.json": { - "timestamp": "2025-10-01T19:33:04.696Z", + "timestamp": "2025-11-19T20:51:08.093Z", "disclosures": [ { "identifier": "prebid:topics", diff --git a/metadata/modules/toponBidAdapter.json b/metadata/modules/toponBidAdapter.json new file mode 100644 index 00000000000..32944cafd7b --- /dev/null +++ b/metadata/modules/toponBidAdapter.json @@ -0,0 +1,18 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": { + "https://mores.toponad.net/tmp/tpn/toponads_tcf_disclosure.json": { + "timestamp": "2025-11-19T20:51:50.224Z", + "disclosures": [] + } + }, + "components": [ + { + "componentType": "bidder", + "componentName": "topon", + "aliasOf": null, + "gvlid": 1305, + "disclosureURL": "https://mores.toponad.net/tmp/tpn/toponads_tcf_disclosure.json" + } + ] +} \ No newline at end of file diff --git a/metadata/modules/tripleliftBidAdapter.json b/metadata/modules/tripleliftBidAdapter.json index 40dc0bd521d..e6ad4e4afce 100644 --- a/metadata/modules/tripleliftBidAdapter.json +++ b/metadata/modules/tripleliftBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://triplelift.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.308Z", + "timestamp": "2025-11-19T20:51:50.269Z", "disclosures": [] } }, diff --git a/metadata/modules/ttdBidAdapter.json b/metadata/modules/ttdBidAdapter.json index d64d812b555..ef523655772 100644 --- a/metadata/modules/ttdBidAdapter.json +++ b/metadata/modules/ttdBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-10-01T19:33:42.365Z", + "timestamp": "2025-11-19T20:51:50.328Z", "disclosures": [] } }, diff --git a/metadata/modules/twistDigitalBidAdapter.json b/metadata/modules/twistDigitalBidAdapter.json index 820e269a5e1..41d3ecd154e 100644 --- a/metadata/modules/twistDigitalBidAdapter.json +++ b/metadata/modules/twistDigitalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://twistdigital.net/iab.json": { - "timestamp": "2025-10-01T19:33:42.365Z", + "timestamp": "2025-11-19T20:51:50.328Z", "disclosures": [ { "identifier": "vdzj1_{id}", diff --git a/metadata/modules/underdogmediaBidAdapter.json b/metadata/modules/underdogmediaBidAdapter.json index 682f379034c..f5b3989c181 100644 --- a/metadata/modules/underdogmediaBidAdapter.json +++ b/metadata/modules/underdogmediaBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.underdog.media/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.425Z", + "timestamp": "2025-11-19T20:51:50.429Z", "disclosures": [] } }, diff --git a/metadata/modules/undertoneBidAdapter.json b/metadata/modules/undertoneBidAdapter.json index 4fc0c0c1b3b..4f7aa901969 100644 --- a/metadata/modules/undertoneBidAdapter.json +++ b/metadata/modules/undertoneBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.undertone.com/js/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.443Z", + "timestamp": "2025-11-19T20:51:50.453Z", "disclosures": [] } }, diff --git a/metadata/modules/unifiedIdSystem.json b/metadata/modules/unifiedIdSystem.json index 11e414f66e2..56adb8f75ad 100644 --- a/metadata/modules/unifiedIdSystem.json +++ b/metadata/modules/unifiedIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ttd-misc-public-assets.s3.us-west-2.amazonaws.com/deviceStorageDisclosureURL.json": { - "timestamp": "2025-10-01T19:33:42.503Z", + "timestamp": "2025-11-19T20:51:50.469Z", "disclosures": [] } }, diff --git a/metadata/modules/uniquest_widgetBidAdapter.json b/metadata/modules/uniquest_widgetBidAdapter.json new file mode 100644 index 00000000000..6caf1c1516d --- /dev/null +++ b/metadata/modules/uniquest_widgetBidAdapter.json @@ -0,0 +1,13 @@ +{ + "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", + "disclosures": {}, + "components": [ + { + "componentType": "bidder", + "componentName": "uniquest_widget", + "aliasOf": null, + "gvlid": null, + "disclosureURL": null + } + ] +} \ No newline at end of file diff --git a/metadata/modules/unrulyBidAdapter.json b/metadata/modules/unrulyBidAdapter.json index ffb1ac8bd7e..bb5a9485c71 100644 --- a/metadata/modules/unrulyBidAdapter.json +++ b/metadata/modules/unrulyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://video.unrulymedia.com/deviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:42.503Z", + "timestamp": "2025-11-19T20:51:50.470Z", "disclosures": [] } }, diff --git a/metadata/modules/userId.json b/metadata/modules/userId.json index 3a553171b24..1bade253a58 100644 --- a/metadata/modules/userId.json +++ b/metadata/modules/userId.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/userId-optout.json": { - "timestamp": "2025-10-01T19:33:04.698Z", + "timestamp": "2025-11-19T20:51:08.095Z", "disclosures": [ { "identifier": "_pbjs_id_optout", diff --git a/metadata/modules/utiqIdSystem.json b/metadata/modules/utiqIdSystem.json index ccb9b088cf3..bbfc0ae04cb 100644 --- a/metadata/modules/utiqIdSystem.json +++ b/metadata/modules/utiqIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/utiqDeviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:42.504Z", + "timestamp": "2025-11-19T20:51:50.470Z", "disclosures": [ { "identifier": "utiqPass", diff --git a/metadata/modules/utiqMtpIdSystem.json b/metadata/modules/utiqMtpIdSystem.json index 1bc24b967ef..1b88f4f1597 100644 --- a/metadata/modules/utiqMtpIdSystem.json +++ b/metadata/modules/utiqMtpIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/modules/utiqDeviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:42.504Z", + "timestamp": "2025-11-19T20:51:50.471Z", "disclosures": [ { "identifier": "utiqPass", diff --git a/metadata/modules/validationFpdModule.json b/metadata/modules/validationFpdModule.json index 0525ce89d7c..60290f5246e 100644 --- a/metadata/modules/validationFpdModule.json +++ b/metadata/modules/validationFpdModule.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.jsdelivr.net/gh/prebid/Prebid.js/metadata/disclosures/prebid/sharedId-optout.json": { - "timestamp": "2025-10-01T19:33:04.697Z", + "timestamp": "2025-11-19T20:51:08.094Z", "disclosures": [ { "identifier": "_pubcid_optout", diff --git a/metadata/modules/valuadBidAdapter.json b/metadata/modules/valuadBidAdapter.json index c51c589ea24..dd16d6190e0 100644 --- a/metadata/modules/valuadBidAdapter.json +++ b/metadata/modules/valuadBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.valuad.cloud/tcfdevice.json": { - "timestamp": "2025-10-01T19:33:42.504Z", + "timestamp": "2025-11-19T20:51:50.471Z", "disclosures": [] } }, diff --git a/metadata/modules/vidazooBidAdapter.json b/metadata/modules/vidazooBidAdapter.json index 54fc6029dbf..18fb84cd5f1 100644 --- a/metadata/modules/vidazooBidAdapter.json +++ b/metadata/modules/vidazooBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vidazoo.com/gdpr-tcf/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:42.836Z", + "timestamp": "2025-11-19T20:51:50.772Z", "disclosures": [ { "identifier": "ck48wz12sqj7", diff --git a/metadata/modules/vidoomyBidAdapter.json b/metadata/modules/vidoomyBidAdapter.json index dba1fad78cc..0d89d6b88ac 100644 --- a/metadata/modules/vidoomyBidAdapter.json +++ b/metadata/modules/vidoomyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vidoomy.com/storageurl/devicestoragediscurl.json": { - "timestamp": "2025-10-01T19:33:42.891Z", + "timestamp": "2025-11-19T20:51:50.839Z", "disclosures": [] } }, diff --git a/metadata/modules/viouslyBidAdapter.json b/metadata/modules/viouslyBidAdapter.json index 1a42e5cabf8..52ef397af3b 100644 --- a/metadata/modules/viouslyBidAdapter.json +++ b/metadata/modules/viouslyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://bid.bricks-co.com/.well-known/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:43.058Z", + "timestamp": "2025-11-19T20:51:51.075Z", "disclosures": [ { "identifier": "fastCMP-addtlConsent", diff --git a/metadata/modules/visxBidAdapter.json b/metadata/modules/visxBidAdapter.json index 3eb1f24b6e9..079ec87a9a4 100644 --- a/metadata/modules/visxBidAdapter.json +++ b/metadata/modules/visxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.yoc.com/visx/sellers/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:43.058Z", + "timestamp": "2025-11-19T20:51:51.077Z", "disclosures": [ { "identifier": "__vads", diff --git a/metadata/modules/vlybyBidAdapter.json b/metadata/modules/vlybyBidAdapter.json index d8f82f8b837..66ed24bcdfa 100644 --- a/metadata/modules/vlybyBidAdapter.json +++ b/metadata/modules/vlybyBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.vlyby.com/conf/iab/gvl.json": { - "timestamp": "2025-10-01T19:33:43.226Z", + "timestamp": "2025-11-19T20:51:51.275Z", "disclosures": [] } }, diff --git a/metadata/modules/voxBidAdapter.json b/metadata/modules/voxBidAdapter.json index 5dca76af986..5f5ff6e0aca 100644 --- a/metadata/modules/voxBidAdapter.json +++ b/metadata/modules/voxBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://st.hybrid.ai/policy/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:43.439Z", + "timestamp": "2025-11-19T20:51:51.299Z", "disclosures": [] } }, diff --git a/metadata/modules/vrtcalBidAdapter.json b/metadata/modules/vrtcalBidAdapter.json index 9fc920f2b43..53853ab1808 100644 --- a/metadata/modules/vrtcalBidAdapter.json +++ b/metadata/modules/vrtcalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://vrtcal.com/docs/gdpr-tcf-disclosures.json": { - "timestamp": "2025-10-01T19:33:43.440Z", + "timestamp": "2025-11-19T20:51:51.299Z", "disclosures": [] } }, diff --git a/metadata/modules/vuukleBidAdapter.json b/metadata/modules/vuukleBidAdapter.json index 3700179addf..ffe75e2969c 100644 --- a/metadata/modules/vuukleBidAdapter.json +++ b/metadata/modules/vuukleBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn.vuukle.com/data-privacy/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:43.647Z", + "timestamp": "2025-11-19T20:51:51.516Z", "disclosures": [ { "identifier": "vuukle_token", diff --git a/metadata/modules/weboramaRtdProvider.json b/metadata/modules/weboramaRtdProvider.json index 00edc78500f..00865923c62 100644 --- a/metadata/modules/weboramaRtdProvider.json +++ b/metadata/modules/weboramaRtdProvider.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://weborama.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:43.914Z", + "timestamp": "2025-11-19T20:51:51.832Z", "disclosures": [] } }, diff --git a/metadata/modules/welectBidAdapter.json b/metadata/modules/welectBidAdapter.json index dcc36ac6a99..60162f3b72a 100644 --- a/metadata/modules/welectBidAdapter.json +++ b/metadata/modules/welectBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://www.welect.de/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:44.170Z", + "timestamp": "2025-11-19T20:51:52.087Z", "disclosures": [] } }, diff --git a/metadata/modules/yahooAdsBidAdapter.json b/metadata/modules/yahooAdsBidAdapter.json index cde1a930470..f4c7bafdc37 100644 --- a/metadata/modules/yahooAdsBidAdapter.json +++ b/metadata/modules/yahooAdsBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://meta.legal.yahoo.com/iab-tcf/v2/device-storage-disclosure.json": { - "timestamp": "2025-10-01T19:33:44.556Z", + "timestamp": "2025-11-19T20:51:52.466Z", "disclosures": [ { "identifier": "vmcid", diff --git a/metadata/modules/yieldlabBidAdapter.json b/metadata/modules/yieldlabBidAdapter.json index 6ccd19b8ed1..3dfd82d0c46 100644 --- a/metadata/modules/yieldlabBidAdapter.json +++ b/metadata/modules/yieldlabBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://ad.yieldlab.net/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:44.556Z", + "timestamp": "2025-11-19T20:51:52.467Z", "disclosures": [] } }, diff --git a/metadata/modules/yieldloveBidAdapter.json b/metadata/modules/yieldloveBidAdapter.json index f4c2f95baa0..56b0890884e 100644 --- a/metadata/modules/yieldloveBidAdapter.json +++ b/metadata/modules/yieldloveBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://cdn-a.yieldlove.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:44.662Z", + "timestamp": "2025-11-19T20:51:52.573Z", "disclosures": [ { "identifier": "session_id", diff --git a/metadata/modules/yieldmoBidAdapter.json b/metadata/modules/yieldmoBidAdapter.json index 990a674fdbf..c52a5ce1d5d 100644 --- a/metadata/modules/yieldmoBidAdapter.json +++ b/metadata/modules/yieldmoBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://devicestoragedisclosureurl.yieldmo.com/deviceStorage.json": { - "timestamp": "2025-10-01T19:33:44.686Z", + "timestamp": "2025-11-19T20:51:52.596Z", "disclosures": [] } }, diff --git a/metadata/modules/zeotapIdPlusIdSystem.json b/metadata/modules/zeotapIdPlusIdSystem.json index 92dd83b8758..efb60121dd1 100644 --- a/metadata/modules/zeotapIdPlusIdSystem.json +++ b/metadata/modules/zeotapIdPlusIdSystem.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://spl.zeotap.com/assets/iab-disclosure.json": { - "timestamp": "2025-10-01T19:33:44.770Z", + "timestamp": "2025-11-19T20:51:52.678Z", "disclosures": [] } }, diff --git a/metadata/modules/zeta_globalBidAdapter.json b/metadata/modules/zeta_globalBidAdapter.json index 46b27b1f38c..a6ae367f9b8 100644 --- a/metadata/modules/zeta_globalBidAdapter.json +++ b/metadata/modules/zeta_globalBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://zetaglobal.com/ZetaDeviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:44.879Z", + "timestamp": "2025-11-19T20:51:52.801Z", "disclosures": [] } }, diff --git a/metadata/modules/zeta_global_sspBidAdapter.json b/metadata/modules/zeta_global_sspBidAdapter.json index f3628c5d616..0f39ead30bf 100644 --- a/metadata/modules/zeta_global_sspBidAdapter.json +++ b/metadata/modules/zeta_global_sspBidAdapter.json @@ -2,7 +2,7 @@ "NOTICE": "do not edit - this file is autogenerated by `gulp update-metadata`", "disclosures": { "https://zetaglobal.com/ZetaDeviceStorageDisclosure.json": { - "timestamp": "2025-10-01T19:33:44.935Z", + "timestamp": "2025-11-19T20:51:52.932Z", "disclosures": [] } }, diff --git a/metadata/overrides.mjs b/metadata/overrides.mjs index 869069f94d3..bb722cfd4f2 100644 --- a/metadata/overrides.mjs +++ b/metadata/overrides.mjs @@ -16,5 +16,6 @@ export default { operaadsIdSystem: 'operaId', relevadRtdProvider: 'RelevadRTDModule', sirdataRtdProvider: 'SirdataRTDModule', - fanBidAdapter: 'freedomadnetwork' + fanBidAdapter: 'freedomadnetwork', + uniquestWidgetBidAdapter: 'uniquest_widget' } diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index fd667799064..1c3241ef19d 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -267,7 +267,7 @@ function handlerAuctionInit(event) { ban_szs: bannerSizes.join(','), bdrs: sortedBidderNames.join(','), pgtyp: deepAccess(event.bidderRequests[0], 'ortb2.site.ext.data.pagetype', null), - plcmt: deepAccess(adUnits[0], 'ortb2Imp.ext.data.placement', null), + plcmt: deepAccess(adUnits[0], 'ortb2Imp.ext.data.adg_rtd.placement', null), // adg_rtd.placement is set by AdagioRtdProvider. t_n: adgRtdSession.testName || null, t_v: adgRtdSession.testVersion || null, s_id: adgRtdSession.id || null, @@ -289,6 +289,11 @@ function handlerAuctionInit(event) { // for backward compatibility: if we didn't find organizationId & site but we have a bid from adagio we might still find it in params qp.org_id = qp.org_id || adagioAdUnitBids[0].params.organizationId; qp.site = qp.site || adagioAdUnitBids[0].params.site; + + // `qp.plcmt` uses the value set by the AdagioRtdProvider. If not present, we fallback on the value set at the adUnit.params level. + if (!qp.plcmt) { + qp.plcmt = deepAccess(adagioAdUnitBids[0], 'params.placement', null); + } } } diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 0cd9ef6ec8a..9d04e166600 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -400,11 +400,18 @@ function autoFillParams(bid) { bid.params.site = adgGlobalConf.siteId.split(':')[1]; } - // `useAdUnitCodeAsPlacement` is an edge case. Useful when a Prebid Manager cannot handle properly params setting. - // In Prebid.js 9, `placement` should be defined in ortb2Imp and the `useAdUnitCodeAsPlacement` param should be removed - bid.params.placement = deepAccess(bid, 'ortb2Imp.ext.data.placement', bid.params.placement); - if (!bid.params.placement && (adgGlobalConf.useAdUnitCodeAsPlacement === true || bid.params.useAdUnitCodeAsPlacement === true)) { - bid.params.placement = bid.adUnitCode; + if (!bid.params.placement) { + let p = deepAccess(bid, 'ortb2Imp.ext.data.adg_rtd.placement', ''); + if (!p) { + // Use ortb2Imp.ext.data.placement for backward compatibility. + p = deepAccess(bid, 'ortb2Imp.ext.data.placement', ''); + } + + // `useAdUnitCodeAsPlacement` is an edge case. Useful when a Prebid Manager cannot handle properly params setting. + if (!p && bid.params.useAdUnitCodeAsPlacement === true) { + p = bid.adUnitCode; + } + bid.params.placement = p; } bid.params.adUnitElementId = deepAccess(bid, 'ortb2Imp.ext.data.divId', bid.params.adUnitElementId); @@ -710,7 +717,7 @@ export const spec = { const requests = Object.keys(groupedAdUnits).map(organizationId => { return { method: 'POST', - url: ENDPOINT, + url: `${ENDPOINT}?orgid=${organizationId}`, data: { organizationId: organizationId, hasRtd: _internal.hasRtd() ? 1 : 0, @@ -738,7 +745,7 @@ export const spec = { usIfr: canSyncWithIframe }, options: { - contentType: 'text/plain' + endpointCompression: true } }; }); diff --git a/modules/adagioRtdProvider.js b/modules/adagioRtdProvider.js index b30be4daf99..dfc6361234e 100644 --- a/modules/adagioRtdProvider.js +++ b/modules/adagioRtdProvider.js @@ -49,7 +49,7 @@ export const PLACEMENT_SOURCES = { }; export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); -const { logError, logWarn } = prefixLog('AdagioRtdProvider:'); +const { logError, logInfo, logWarn } = prefixLog('AdagioRtdProvider:'); // Guard to avoid storing the same bid data several times. const guard = new Set(); @@ -240,6 +240,25 @@ export const _internal = { return value; } }); + }, + + // Compute the placement from the legacy RTD config params or ortb2Imp.ext.data.placement key. + computePlacementFromLegacy: function(rtdConfig, adUnit) { + const placementSource = deepAccess(rtdConfig, 'params.placementSource', ''); + let placementFromSource = ''; + + switch (placementSource.toLowerCase()) { + case PLACEMENT_SOURCES.ADUNITCODE: + placementFromSource = adUnit.code; + break; + case PLACEMENT_SOURCES.GPID: + placementFromSource = deepAccess(adUnit, 'ortb2Imp.ext.gpid') + break; + } + + const placementLegacy = deepAccess(adUnit, 'ortb2Imp.ext.data.placement', ''); + + return placementFromSource || placementLegacy; } }; @@ -319,7 +338,6 @@ function onBidRequest(bidderRequest, config, _userConsent) { * @param {*} config */ function onGetBidRequestData(bidReqConfig, callback, config) { - const configParams = deepAccess(config, 'params', {}); const { site: ortb2Site } = bidReqConfig.ortb2Fragments.global; const features = _internal.getFeatures().get(); const ext = { @@ -347,30 +365,11 @@ function onGetBidRequestData(bidReqConfig, callback, config) { const slotPosition = getSlotPosition(divId); deepSetValue(ortb2Imp, `ext.data.adg_rtd.adunit_position`, slotPosition); - // It is expected that the publisher set a `adUnits[].ortb2Imp.ext.data.placement` value. - // Btw, We allow fallback sources to programmatically set this value. - // The source is defined in the `config.params.placementSource` and the possible values are `code` or `gpid`. - // (Please note that this `placement` is not related to the oRTB video property.) - if (!deepAccess(ortb2Imp, 'ext.data.placement')) { - const { placementSource = '' } = configParams; - - switch (placementSource.toLowerCase()) { - case PLACEMENT_SOURCES.ADUNITCODE: - deepSetValue(ortb2Imp, 'ext.data.placement', adUnit.code); - break; - case PLACEMENT_SOURCES.GPID: - deepSetValue(ortb2Imp, 'ext.data.placement', deepAccess(ortb2Imp, 'ext.gpid')); - break; - default: - logWarn('`ortb2Imp.ext.data.placement` is missing and `params.definePlacement` is not set in the config.'); - } - } - - // We expect that `pagetype`, `category`, `placement` are defined in FPD `ortb2.site.ext.data` and `adUnits[].ortb2Imp.ext.data` objects. - // Btw, we have to ensure compatibility with publishers that use the "legacy" adagio params at the adUnit.params level. const adagioBid = adUnit.bids.find(bid => _internal.isAdagioBidder(bid.bidder)); if (adagioBid) { // ortb2 level + // We expect that `pagetype`, `category` are defined in FPD `ortb2.site.ext.data` object. + // Btw, we still ensure compatibility with publishers that use the adagio params at the adUnit.params level. let mustWarnOrtb2 = false; if (!deepAccess(ortb2Site, 'ext.data.pagetype') && adagioBid.params.pagetype) { deepSetValue(ortb2Site, 'ext.data.pagetype', adagioBid.params.pagetype); @@ -380,21 +379,28 @@ function onGetBidRequestData(bidReqConfig, callback, config) { deepSetValue(ortb2Site, 'ext.data.category', adagioBid.params.category); mustWarnOrtb2 = true; } - - // ortb2Imp level - let mustWarnOrtb2Imp = false; - if (!deepAccess(ortb2Imp, 'ext.data.placement')) { - if (adagioBid.params.placement) { - deepSetValue(ortb2Imp, 'ext.data.placement', adagioBid.params.placement); - mustWarnOrtb2Imp = true; - } + if (mustWarnOrtb2) { + logInfo('`pagetype` and/or `category` have been set in the FPD `ortb2.site.ext.data` object from `adUnits[].bids.adagio.params`.'); } - if (mustWarnOrtb2) { - logWarn('`pagetype` and `category` must be defined in the FPD `ortb2.site.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + // ortb2Imp level to handle legacy. + // The `placement` is finally set at the adUnit.params level (see https://github.com/prebid/Prebid.js/issues/12845) + // but we still need to set it at the ortb2Imp level for our internal use. + const placementParam = adagioBid.params.placement; + const adgRtdPlacement = deepAccess(ortb2Imp, 'ext.data.adg_rtd.placement', ''); + + if (placementParam) { + // Always overwrite the ortb2Imp value with the one from the adagio adUnit.params.placement if defined. + // This is the common case. + deepSetValue(ortb2Imp, 'ext.data.adg_rtd.placement', placementParam); } - if (mustWarnOrtb2Imp) { - logWarn('`placement` must be defined in the FPD `adUnits[].ortb2Imp.ext.data` object. Relying on `adUnits[].bids.adagio.params` is deprecated.'); + + if (!placementParam && !adgRtdPlacement) { + const p = _internal.computePlacementFromLegacy(config, adUnit); + if (p) { + deepSetValue(ortb2Imp, 'ext.data.adg_rtd.placement', p); + logWarn('`ortb2Imp.ext.data.adg_rtd.placement` has been set from a legacy source. Please set `bids[].adagio.params.placement` or `ortb2Imp.ext.data.adg_rtd.placement` value.'); + } } } }); diff --git a/modules/adgridBidAdapter.js b/modules/adgridBidAdapter.js deleted file mode 100644 index d1cccf21c52..00000000000 --- a/modules/adgridBidAdapter.js +++ /dev/null @@ -1,133 +0,0 @@ -import { deepSetValue, generateUUID, logInfo } from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { ortbConverter } from '../libraries/ortbConverter/converter.js' -import { createResponse, enrichImp, enrichRequest, getAmxId, getUserSyncs } from '../libraries/nexx360Utils/index.js'; - -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse - * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions - * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync - * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests - */ - -const BIDDER_CODE = 'adgrid'; -const REQUEST_URL = 'https://fast.nexx360.io/adgrid'; -const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '2.0'; -const ADGRID_KEY = 'adgrid'; - -const ALIASES = []; - -// Define the storage manager for the Adgrid bidder -export const STORAGE = getStorageManager({ - bidderCode: BIDDER_CODE, -}); - -/** - * Get the agdridId from local storage - * @return {object | false } false if localstorageNotEnabled - */ -export function getLocalStorage() { - if (!STORAGE.localStorageIsEnabled()) { - logInfo(`localstorage not enabled for Adgrid`); - return false; - } - const output = STORAGE.getDataFromLocalStorage(ADGRID_KEY); - if (output === null) { - const adgridStorage = { adgridId: generateUUID() }; - STORAGE.setDataInLocalStorage(ADGRID_KEY, JSON.stringify(adgridStorage)); - return adgridStorage; - } - try { - return JSON.parse(output); - } catch (e) { - return false; - } -} - -const converter = ortbConverter({ - context: { - netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false - ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) - }, - imp(buildImp, bidRequest, context) { - let imp = buildImp(bidRequest, context); - imp = enrichImp(imp, bidRequest); - if (bidRequest.params.domainId) deepSetValue(imp, 'ext.adgrid.domainId', bidRequest.params.domainId); - if (bidRequest.params.placement) deepSetValue(imp, 'ext.adgrid.placement', bidRequest.params.placement); - return imp; - }, - request(buildRequest, imps, bidderRequest, context) { - let request = buildRequest(imps, bidderRequest, context); - const amxId = getAmxId(STORAGE, BIDDER_CODE); - request = enrichRequest(request, amxId, bidderRequest, PAGE_VIEW_ID, BIDDER_VERSION); - return request; - }, -}); - -/** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ -function isBidRequestValid(bid) { - if (!bid || !bid.params) return false; - if (typeof bid.params.domainId !== 'number') return false; - if (typeof bid.params.placement !== 'string') return false; - return true; -} - -/** - * Make a server request from the list of BidRequests. - * - * @return ServerRequest Info describing the request to the server. - */ -function buildRequests(bidRequests, bidderRequest) { - const data = converter.toORTB({ bidRequests, bidderRequest }) - return { - method: 'POST', - url: REQUEST_URL, - data, - } -} - -/** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ -function interpretResponse(serverResponse) { - const respBody = serverResponse.body; - if (!respBody || !Array.isArray(respBody.seatbid)) { - return []; - } - - const responses = []; - for (let i = 0; i < respBody.seatbid.length; i++) { - const seatbid = respBody.seatbid[i]; - for (let j = 0; j < seatbid.bid.length; j++) { - const bid = seatbid.bid[j]; - const response = createResponse(bid, respBody); - responses.push(response); - } - } - return responses; -} - -export const spec = { - code: BIDDER_CODE, - aliases: ALIASES, - supportedMediaTypes: [BANNER, VIDEO], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs, -}; - -registerBidder(spec); diff --git a/modules/adgridBidAdapter.ts b/modules/adgridBidAdapter.ts new file mode 100644 index 00000000000..e5ba2c8b672 --- /dev/null +++ b/modules/adgridBidAdapter.ts @@ -0,0 +1,102 @@ +import { deepSetValue, generateUUID } from '../src/utils.js'; +import { getStorageManager, StorageManager } from '../src/storageManager.js'; +import { AdapterRequest, BidderSpec, registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; +import { interpretResponse, enrichImp, enrichRequest, getAmxId, getLocalStorageFunctionGenerator, getUserSyncs } from '../libraries/nexx360Utils/index.js'; +import { ORTBRequest } from '../src/prebid.public.js'; + +const BIDDER_CODE = 'adgrid'; +const REQUEST_URL = 'https://fast.nexx360.io/adgrid'; +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '2.0'; +const ADGRID_KEY = 'adgrid'; + +type RequireAtLeastOne = + Omit & { + [K in Keys]-?: Required> & + Partial>> + }[Keys]; + +type AdgridBidParams = RequireAtLeastOne<{ + domainId?: string; + placement?: string; + allBids?: boolean; + customId?: string; +}, "domainId" | "placement">; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: AdgridBidParams; + } +} + +const ALIASES = []; + +// Define the storage manager for the Adgrid bidder +export const STORAGE: StorageManager = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +export const getAdgridLocalStorage = getLocalStorageFunctionGenerator<{ adgridId: string }>( + STORAGE, + BIDDER_CODE, + ADGRID_KEY, + 'adgridId' +); + +const converter = ortbConverter({ + context: { + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + imp = enrichImp(imp, bidRequest); + const params = bidRequest.params as AdgridBidParams; + if (params.domainId) deepSetValue(imp, 'ext.adgrid.domainId', params.domainId); + if (params.placement) deepSetValue(imp, 'ext.adgrid.placement', params.placement); + if (params.allBids) deepSetValue(imp, 'ext.adgrid.allBids', params.allBids); + if (params.customId) deepSetValue(imp, 'ext.adgrid.customId', params.customId); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + let request = buildRequest(imps, bidderRequest, context); + const amxId = getAmxId(STORAGE, BIDDER_CODE); + request = enrichRequest(request, amxId, PAGE_VIEW_ID, BIDDER_VERSION); + return request; + }, +}); + +const isBidRequestValid = (bid:BidRequest): boolean => { + if (!bid || !bid.params) return false; + if (typeof bid.params.domainId !== 'number') return false; + if (typeof bid.params.placement !== 'string') return false; + return true; +} + +const buildRequests = ( + bidRequests: BidRequest[], + bidderRequest: ClientBidderRequest, +): AdapterRequest => { + const data:ORTBRequest = converter.toORTB({bidRequests, bidderRequest}) + const adapterRequest:AdapterRequest = { + method: 'POST', + url: REQUEST_URL, + data, + } + return adapterRequest; +} + +export const spec:BidderSpec = { + code: BIDDER_CODE, + aliases: ALIASES, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 3bfaeeff712..4bf011cc12c 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { _each, contains, @@ -105,7 +105,9 @@ export const spec = { {code: 'pixelpluses', gvlid: 1209}, {code: 'urekamedia'}, {code: 'smartyexchange'}, - {code: 'infinety'} + {code: 'infinety'}, + {code: 'qohere'}, + {code: 'blutonic'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index 17b335d3cbe..4a4556cfb1e 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -14,7 +14,6 @@ import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; import {toLegacyResponse, toOrtbNativeRequest} from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getExtraWinDimensions} from '../libraries/extraWinDimensions/extraWinDimensions.js'; const BIDDER_CODE = 'adnuntius'; const BIDDER_CODE_DEAL_ALIAS_BASE = 'adndeal'; @@ -304,10 +303,6 @@ export const spec = { queryParamsAndValues.push('consentString=' + consentString); queryParamsAndValues.push('gdpr=' + flag); } - const extraDims = getExtraWinDimensions(); - if (extraDims.screen.availHeight) { - queryParamsAndValues.push('screen=' + extraDims.screen.availWidth + 'x' + extraDims.screen.availHeight); - } const { innerWidth, innerHeight } = getWinDimensions(); diff --git a/modules/adoceanBidAdapter.js b/modules/adoceanBidAdapter.js new file mode 100644 index 00000000000..6f6548a1ba8 --- /dev/null +++ b/modules/adoceanBidAdapter.js @@ -0,0 +1,164 @@ +import { _each, isStr, isArray, parseSizesInput } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adocean'; +const URL_SAFE_FIELDS = { + slaves: true +}; + +function buildEndpointUrl(emitter, payloadMap) { + const payload = []; + _each(payloadMap, function(v, k) { + payload.push(k + '=' + (URL_SAFE_FIELDS[k] ? v : encodeURIComponent(v))); + }); + + const randomizedPart = Math.random().toString().slice(2); + return 'https://' + emitter + '/_' + randomizedPart + '/ad.json?' + payload.join('&'); +} + +function buildRequest(bid, gdprConsent) { + const emitter = bid.params.emitter; + const masterId = bid.params.masterId; + const slaveId = bid.params.slaveId; + const payload = { + id: masterId, + slaves: "" + }; + if (gdprConsent) { + payload.gdpr_consent = gdprConsent.consentString || undefined; + payload.gdpr = gdprConsent.gdprApplies ? 1 : 0; + } + + if (bid.userId && bid.userId.gemiusId) { + payload.aouserid = bid.userId.gemiusId; + } + + const bidIdMap = {}; + const uniquePartLength = 10; + + const rawSlaveId = bid.params.slaveId.replace('adocean', ''); + payload.slaves = rawSlaveId.slice(-uniquePartLength); + + bidIdMap[slaveId] = bid.bidId; + + if (bid.mediaTypes.video) { + if (bid.mediaTypes.video.context === 'instream') { + if (bid.mediaTypes.video.maxduration) { + payload.dur = bid.mediaTypes.video.maxduration; + payload.maxdur = bid.mediaTypes.video.maxduration; + } + if (bid.mediaTypes.video.minduration) { + payload.mindur = bid.mediaTypes.video.minduration; + } + payload.spots = 1; + } + if (bid.mediaTypes.video.context === 'adpod') { + const durationRangeSec = bid.mediaTypes.video.durationRangeSec; + if (!bid.mediaTypes.video.adPodDurationSec || !isArray(durationRangeSec) || durationRangeSec.length === 0) { + return; + } + const spots = calculateAdPodSpotsNumber(bid.mediaTypes.video.adPodDurationSec, bid.mediaTypes.video.durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); + payload.dur = bid.mediaTypes.video.adPodDurationSec; + payload.maxdur = maxDuration; + payload.spots = spots; + } + } else if (bid.mediaTypes.banner) { + payload.aosize = parseSizesInput(bid.mediaTypes.banner.sizes).join(','); + } + + return { + method: 'GET', + url: buildEndpointUrl(emitter, payload), + data: '', + bidIdMap: bidIdMap + }; +} + +function calculateAdPodSpotsNumber(adPodDurationSec, durationRangeSec) { + const minAllowedDuration = Math.min(...durationRangeSec); + const numberOfSpots = Math.floor(adPodDurationSec / minAllowedDuration); + return numberOfSpots; +} + +function interpretResponse(placementResponse, bidRequest, bids) { + const requestId = bidRequest.bidIdMap[placementResponse.id]; + if (!placementResponse.error && requestId) { + if (!placementResponse.code || !placementResponse.height || !placementResponse.width || !placementResponse.price) { + return; + } + let adCode = decodeURIComponent(placementResponse.code); + + const bid = { + cpm: parseFloat(placementResponse.price), + currency: placementResponse.currency, + height: parseInt(placementResponse.height, 10), + requestId: requestId, + width: parseInt(placementResponse.width, 10), + netRevenue: false, + ttl: parseInt(placementResponse.ttl), + creativeId: placementResponse.crid, + meta: { + advertiserDomains: placementResponse.adomain || [] + } + }; + if (placementResponse.isVideo) { + bid.meta.mediaType = VIDEO; + bid.vastXml = adCode; + } else { + bid.meta.mediaType = BANNER; + bid.ad = adCode; + } + + bids.push(bid); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function(bid) { + const requiredParams = ['slaveId', 'masterId', 'emitter']; + if (requiredParams.some(name => !isStr(bid.params[name]) || !bid.params[name].length)) { + return false; + } + + if (bid.mediaTypes.banner) { + return true; + } + if (bid.mediaTypes.video) { + if (bid.mediaTypes.video.context === 'instream') { + return true; + } + if (bid.mediaTypes.video.context === 'adpod') { + return !bid.mediaTypes.video.requireExactDuration; + } + } + return false; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + + _each(validBidRequests, function(bidRequest) { + requests.push(buildRequest(bidRequest, bidderRequest.gdprConsent)); + }); + + return requests; + }, + + interpretResponse: function(serverResponse, bidRequest) { + let bids = []; + + if (isArray(serverResponse.body)) { + _each(serverResponse.body, function(placementResponse) { + interpretResponse(placementResponse, bidRequest, bids); + }); + } + + return bids; + } +}; +registerBidder(spec); diff --git a/modules/adoceanBidAdapter.md b/modules/adoceanBidAdapter.md new file mode 100644 index 00000000000..c7b55ddc5c3 --- /dev/null +++ b/modules/adoceanBidAdapter.md @@ -0,0 +1,58 @@ +# Overview + +Module Name: AdOcean Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@gemius.com + +# Description + +AdOcean Bidder Adapter for Prebid.js. +Banner and video formats are supported. + +# Test Parameters Banner +```js + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 200]] + } + }, + bids: [ + { + bidder: "adocean", + params: { + slaveId: 'adoceanmyaotcpiltmmnj', + masterId: 'ek1AWtSWh3BOa_x2P1vlMQ_uXXJpJcbhsHAY5PFQjWD.D7', + emitter: 'myao.adocean.pl' + } + } + ] + } + ]; +``` +# Test Parameters Video +```js + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 200] + } + }, + bids: [ + { + bidder: "adocean", + params: { + slaveId: 'adoceanmyaonenfcoqfnd', + masterId: '2k6gA7RWl08Zn0bi42RV8LNCANpKb6LqhvKzbmK3pzP.U7', + emitter: 'myao.adocean.pl' + } + } + ] + } + ]; +``` diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js index 696234d1b51..2c4278b9fb8 100644 --- a/modules/adtrueBidAdapter.js +++ b/modules/adtrueBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { logWarn, isArray, inIframe, isNumber, isStr, deepClone, deepSetValue, logError, deepAccess, isBoolean } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; diff --git a/modules/adverxoBidAdapter.js b/modules/adverxoBidAdapter.js index 2c24f20fdbc..20ff4acb939 100644 --- a/modules/adverxoBidAdapter.js +++ b/modules/adverxoBidAdapter.js @@ -20,13 +20,15 @@ const BIDDER_CODE = 'adverxo'; const ALIASES = [ {code: 'adport', skipPbsAliasing: true}, - {code: 'bidsmind', skipPbsAliasing: true} + {code: 'bidsmind', skipPbsAliasing: true}, + {code: 'harrenmedia', skipPbsAliasing: true} ]; const AUCTION_URLS = { adverxo: 'js.pbsadverxo.com', adport: 'ayuetina.com', - bidsmind: 'arcantila.com' + bidsmind: 'arcantila.com', + harrenmedia: 'harrenmediaprebid.com' }; const ENDPOINT_URL_AD_UNIT_PLACEHOLDER = '{AD_UNIT}'; diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index 75944516c2d..a00fd698932 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -5,19 +5,24 @@ import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions */ -const BidderCode = 'aja'; -const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; -const SDKType = 5; -const AdType = { +const BIDDER_CODE = 'aja'; +const ENDPOINT_URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; +const SDK_TYPE = 5; + +const AD_TYPE = { Banner: 1, Native: 2, Video: 3, }; -const BannerSizeMap = { +const BANNER_SIZE_MAP = { '970x250': 1, '300x250': 2, '320x50': 3, @@ -25,29 +30,59 @@ const BannerSizeMap = { '320x100': 6, '336x280': 31, '300x600': 32, -} +}; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 300; +const DEFAULT_NET_REVENUE = true; + +/** + * @typedef {object} AJABidResponse + * + * @property {boolean} is_ad_return - Whether an ad was returned + * @property {AJAAd} ad - The ad object + * @property {string[]} [syncs] - Array of user sync pixel URLs + * @property {string[]} [sync_htmls] - Array of user sync iframe URLs + */ + +/** + * @typedef {object} AJAAd + * + * @property {number} ad_type - Type of ad (1=Banner, 2=Native, 3=Video) + * @property {string} prebid_id - Prebid bid ID + * @property {number} price - CPM price + * @property {string} [creative_id] - Creative ID + * @property {string} [deal_id] - Deal ID + * @property {string} [currency] - Currency code + * @property {AJABannerAd} banner - Banner ad data + */ + +/** + * @typedef {object} AJABannerAd + * + * @property {string} tag - HTML tag for the ad + * @property {number} w - Width of the ad + * @property {number} h - Height of the ad + * @property {string[]} [adomain] - Advertiser domains + * @property {string[]} [imps] - Array of impression tracking URLs + */ export const spec = { - code: BidderCode, + code: BIDDER_CODE, supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid has all the params needed to make a valid request. - * * @param {BidRequest} bidRequest * @returns {boolean} */ isBidRequestValid: function(bidRequest) { - return !!(bidRequest.params.asi); + return !!(bidRequest.params?.asi); }, /** - * Build the request to the Server which requests Bids for the given array of Requests. - * Each BidRequest in the argument array is guaranteed to have passed the isBidRequestValid() test. - * * @param {BidRequest[]} validBidRequests - * @param {*} bidderRequest - * @returns {ServerRequest|ServerRequest[]} + * @param {BidderRequest} bidderRequest + * @returns {ServerRequest[]} */ buildRequests: function(validBidRequests, bidderRequest) { const bidRequests = []; @@ -60,17 +95,17 @@ export const spec = { const asi = getBidIdParameter('asi', bidRequest.params); queryString = tryAppendQueryString(queryString, 'asi', asi); - queryString = tryAppendQueryString(queryString, 'skt', SDKType); - queryString = tryAppendQueryString(queryString, 'gpid', bidRequest.ortb2Imp?.ext?.gpid) - queryString = tryAppendQueryString(queryString, 'tid', bidRequest.ortb2Imp?.ext?.tid) - queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep) + queryString = tryAppendQueryString(queryString, 'skt', SDK_TYPE); + queryString = tryAppendQueryString(queryString, 'gpid', bidRequest.ortb2Imp?.ext?.gpid); + queryString = tryAppendQueryString(queryString, 'tid', bidRequest.ortb2Imp?.ext?.tid); + queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep); queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); const schain = bidRequest?.ortb2?.source?.ext?.schain; - queryString = tryAppendQueryString(queryString, 'schain', spec.serializeSupplyChain(schain || [])) + queryString = tryAppendQueryString(queryString, 'schain', spec.serializeSupplyChain(schain || [])); - const adFormatIDs = pickAdFormats(bidRequest) + const adFormatIDs = pickAdFormats(bidRequest); if (adFormatIDs && adFormatIDs.length > 0) { queryString = tryAppendQueryString(queryString, 'ad_format_ids', adFormatIDs.join(',')); } @@ -82,14 +117,14 @@ export const spec = { })); } - const sua = bidRequest.ortb2?.device?.sua + const sua = bidRequest.ortb2?.device?.sua; if (sua) { queryString = tryAppendQueryString(queryString, 'sua', JSON.stringify(sua)); } bidRequests.push({ method: 'GET', - url: URL, + url: ENDPOINT_URL, data: queryString }); } @@ -97,19 +132,28 @@ export const spec = { return bidRequests; }, - interpretResponse: function(bidderResponse) { - const bidderResponseBody = bidderResponse.body; + /** + * @param {ServerResponse} serverResponse + * @param {ServerRequest} bidRequest + * @returns {Bid[]} + */ + interpretResponse: function(serverResponse, bidRequest) { + const bidderResponseBody = serverResponse.body; if (!bidderResponseBody.is_ad_return) { return []; } const ad = bidderResponseBody.ad; - if (AdType.Banner !== ad.ad_type) { - return [] + if (!ad || AD_TYPE.Banner !== ad.ad_type) { + return []; + } + + const bannerAd = ad.banner; + if (!bannerAd) { + return []; } - const bannerAd = bidderResponseBody.ad.banner; const bid = { requestId: ad.prebid_id, mediaType: BANNER, @@ -119,18 +163,21 @@ export const spec = { cpm: ad.price, creativeId: ad.creative_id, dealId: ad.deal_id, - currency: ad.currency || 'USD', - netRevenue: true, - ttl: 300, // 5 minutes + currency: ad.currency || DEFAULT_CURRENCY, + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL, meta: { - advertiserDomains: bannerAd.adomain, + advertiserDomains: bannerAd.adomain || [], }, - } + }; + try { - bannerAd.imps.forEach(impTracker => { - const tracker = createTrackPixelHtml(impTracker); - bid.ad += tracker; - }); + if (Array.isArray(bannerAd.imps)) { + bannerAd.imps.forEach(impTracker => { + const tracker = createTrackPixelHtml(impTracker); + bid.ad += tracker; + }); + } } catch (error) { logError('Error appending tracking pixel', error); } @@ -138,6 +185,11 @@ export const spec = { return [bid]; }, + /** + * @param {SyncOptions} syncOptions + * @param {ServerResponse[]} serverResponses + * @returns {{type: string, url: string}[]} + */ getUserSyncs: function(syncOptions, serverResponses) { const syncs = []; if (!serverResponses.length) { @@ -146,7 +198,7 @@ export const spec = { const bidderResponseBody = serverResponses[0].body; - if (syncOptions.pixelEnabled && bidderResponseBody.syncs) { + if (syncOptions.pixelEnabled && bidderResponseBody.syncs && Array.isArray(bidderResponseBody.syncs)) { bidderResponseBody.syncs.forEach(sync => { syncs.push({ type: 'image', @@ -155,7 +207,7 @@ export const spec = { }); } - if (syncOptions.iframeEnabled && bidderResponseBody.sync_htmls) { + if (syncOptions.iframeEnabled && bidderResponseBody.sync_htmls && Array.isArray(bidderResponseBody.sync_htmls)) { bidderResponseBody.sync_htmls.forEach(sync => { syncs.push({ type: 'iframe', @@ -168,48 +220,52 @@ export const spec = { }, /** - * Serialize supply chain object * @param {Object} supplyChain - * @returns {String | undefined} + * @returns {string|undefined} */ serializeSupplyChain: function(supplyChain) { - if (!supplyChain || !supplyChain.nodes) return undefined - const { ver, complete, nodes } = supplyChain - return `${ver},${complete}!${spec.serializeSupplyChainNodes(nodes)}` + if (!supplyChain || !supplyChain.nodes) { + return undefined; + } + const { ver, complete, nodes } = supplyChain; + return `${ver},${complete}!${spec.serializeSupplyChainNodes(nodes)}`; }, /** - * Serialize each supply chain nodes * @param {Array} nodes - * @returns {String} + * @returns {string} */ serializeSupplyChainNodes: function(nodes) { - const fields = ['asi', 'sid', 'hp', 'rid', 'name', 'domain'] + const fields = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; return nodes.map((n) => { return fields.map((f) => { - return encodeURIComponent(n[f] || '').replace(/!/g, '%21') - }).join(',') - }).join('!') + return encodeURIComponent(n[f] || '').replace(/!/g, '%21'); + }).join(','); + }).join('!'); } -} +}; +/** + * @param {BidRequest} bidRequest + * @returns {number[]} + */ function pickAdFormats(bidRequest) { - const sizes = bidRequest.sizes || [] - sizes.push(...(bidRequest.mediaTypes?.banner?.sizes || [])) + const sizes = bidRequest.sizes || []; + sizes.push(...(bidRequest.mediaTypes?.banner?.sizes || [])); const adFormatIDs = []; for (const size of sizes) { - if (size.length !== 2) { - continue + if (!Array.isArray(size) || size.length !== 2) { + continue; } - const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; + const adFormatID = BANNER_SIZE_MAP[`${size[0]}x${size[1]}`]; if (adFormatID) { adFormatIDs.push(adFormatID); } } - return [...new Set(adFormatIDs)] + return [...new Set(adFormatIDs)]; } registerBidder(spec); diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index 025d677fbd4..8e77e2507b7 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {deepAccess, deepClone, generateUUID, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; @@ -22,21 +22,25 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - const bids = []; - const bidIds = []; + let bids = []; + let bidIds = []; let eids; validBidRequests.forEach(bidRequest => { - const formatTypes = getFormatType(bidRequest) + let formatTypes = getFormatType(bidRequest); + + // Get floor info with currency support + const floorInfo = getBidFloor(bidRequest, formatTypes); if (bidRequest.userIdAsEids) { - eids = eids || bidRequest.userIdAsEids + eids = eids || bidRequest.userIdAsEids; } bids.push({ token: bidRequest.params.token, instl: bidRequest.params.instl, exp: bidRequest.params.exp, - bidFloor: getBidFloor(bidRequest, formatTypes), + bidFloor: floorInfo.floor, // Floor amount + currency: floorInfo.currency, // Floor currency (NEW) sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), impMediaTypes: formatTypes, @@ -44,22 +48,42 @@ export const spec = { video: deepAccess(bidRequest, 'mediaTypes.video'), banner: deepAccess(bidRequest, 'mediaTypes.banner'), ext: bidRequest.ortb2Imp?.ext - }) - bidIds.push(bidRequest.bidId) - }) - - const ortb2 = bidderRequest.ortb2 - const site = ortb2?.site - - const id = getUserId() - const alkimiConfig = config.getConfig('alkimi') - const fpa = ortb2?.source?.ext?.fpa - const source = fpa !== null && fpa !== undefined ? { ext: { fpa } } : undefined - const walletID = alkimiConfig && alkimiConfig.walletID + }); + bidIds.push(bidRequest.bidId); + }); + + const ortb2 = bidderRequest.ortb2; + const site = ortb2?.site; + + const id = getUserId(); + const alkimiConfig = config.getConfig('alkimi'); + const fpa = ortb2?.source?.ext?.fpa; + const source = fpa !== undefined ? { ext: { fpa } } : undefined; + const userWalletAddress = alkimiConfig && alkimiConfig.userWalletAddress const userParams = alkimiConfig && alkimiConfig.userParams - const user = ((walletID !== null && walletID !== undefined) || (userParams !== null && userParams !== undefined) || (id !== null && id !== undefined)) ? { id, ext: { walletID, userParams } } : undefined + const userWalletConnected = alkimiConfig && alkimiConfig.userWalletConnected + const userWalletProtocol = normalizeToArray(alkimiConfig && alkimiConfig.userWalletProtocol) + const userTokenType = normalizeToArray(alkimiConfig && alkimiConfig.userTokenType) + + const user = ((userWalletAddress !== null && userWalletAddress !== undefined) || + (userParams !== null && userParams !== undefined) || + (id !== null && id !== undefined) || + (userWalletConnected !== null && userWalletConnected !== undefined) || + (userWalletProtocol !== null && userWalletProtocol !== undefined) || + (userTokenType !== null && userTokenType !== undefined)) + ? { + id, + ext: { + userWalletAddress, + userParams, + userWalletConnected, + userWalletProtocol, + userTokenType + } + } + : undefined - const payload = { + let payload = { requestId: generateUUID(), signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, @@ -86,13 +110,13 @@ export const spec = { badv: ortb2?.badv, wseat: ortb2?.wseat } - } + }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdprConsent = { consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : false, consentString: bidderRequest.gdprConsent.consentString - } + }; } if (bidderRequest.uspConsent) { @@ -100,7 +124,7 @@ export const spec = { } if (eids) { - payload.eids = eids + payload.eids = eids; } const options = { @@ -108,7 +132,7 @@ export const spec = { customHeaders: { 'Rtb-Direct': true } - } + }; return { method: 'POST', @@ -129,11 +153,14 @@ export const spec = { return []; } - const bids = []; + let bids = []; prebidResponse.forEach(bidResponse => { - const bid = deepClone(bidResponse); + let bid = deepClone(bidResponse); bid.cpm = parseFloat(bidResponse.cpm); + // Set currency from response (NEW - supports multi-currency) + bid.currency = bidResponse.currency || 'USD'; + // banner or video if (VIDEO === bid.mediaType) { bid.vastUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); @@ -143,7 +170,7 @@ export const spec = { bid.meta.advertiserDomains = bid.adomain || []; bids.push(bid); - }) + }); return bids; }, @@ -168,16 +195,16 @@ export const spec = { const urls = []; iframeList.forEach(url => { urls.push({type: 'iframe', url}); - }) + }); return urls; } return []; } -} +}; function prepareSizes(sizes) { - return sizes ? sizes.map(size => ({width: size[0], height: size[1]})) : [] + return sizes ? sizes.map(size => ({width: size[0], height: size[1]})) : []; } function prepareBidFloorSize(sizes) { @@ -185,37 +212,69 @@ function prepareBidFloorSize(sizes) { } function getBidFloor(bidRequest, formatTypes) { - let minFloor + let minFloor; + let floorCurrency; + const currencyConfig = config.getConfig('currency') || {}; + const adServerCurrency = currencyConfig.adServerCurrency || 'USD'; // Default to USD + if (typeof bidRequest.getFloor === 'function') { - const bidFloorSizes = prepareBidFloorSize(bidRequest.sizes) + const bidFloorSizes = prepareBidFloorSize(bidRequest.sizes); formatTypes.forEach(formatType => { bidFloorSizes.forEach(bidFloorSize => { - const floor = bidRequest.getFloor({currency: 'USD', mediaType: formatType.toLowerCase(), size: bidFloorSize}); - if (floor && !isNaN(floor.floor) && (floor.currency === 'USD')) { - minFloor = !minFloor || floor.floor < minFloor ? floor.floor : minFloor + const floor = bidRequest.getFloor({ + currency: adServerCurrency, + mediaType: formatType.toLowerCase(), + size: bidFloorSize + }); + + if (floor && !isNaN(floor.floor)) { + if (!minFloor || floor.floor < minFloor) { + minFloor = floor.floor; + floorCurrency = floor.currency; + } } - }) - }) + }); + }); } - return minFloor || bidRequest.params.bidFloor; + + return { + floor: minFloor || bidRequest.params.bidFloor, + currency: floorCurrency || adServerCurrency + }; } const getFormatType = bidRequest => { - const formats = [] - if (deepAccess(bidRequest, 'mediaTypes.banner')) formats.push('Banner') - if (deepAccess(bidRequest, 'mediaTypes.video')) formats.push('Video') - return formats -} + let formats = []; + if (deepAccess(bidRequest, 'mediaTypes.banner')) formats.push('Banner'); + if (deepAccess(bidRequest, 'mediaTypes.video')) formats.push('Video'); + return formats; +}; const getUserId = () => { if (storage.localStorageIsEnabled()) { - let userId = storage.getDataFromLocalStorage(USER_ID_KEY) + let userId = storage.getDataFromLocalStorage(USER_ID_KEY); if (!userId) { - userId = generateUUID() - storage.setDataInLocalStorage(USER_ID_KEY, userId) + userId = generateUUID(); + storage.setDataInLocalStorage(USER_ID_KEY, userId); } - return userId + return userId; } +}; + +function normalizeToArray(value) { + if (!value) { + return undefined; + } + + if (Array.isArray(value)) { + return value; + } + + if (typeof value === 'string') { + return value.split(',').map(item => item.trim()).filter(item => item.length > 0); + } + + return [value]; } registerBidder(spec); diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index b1b22ec19f9..ef89ecdd40f 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -251,7 +251,7 @@ function getSyncSettings() { const all = isSyncEnabled(syncConfig.filterSettings, 'all'); if (all) { - settings.t = SYNC_IMAGE & SYNC_IFRAME; + settings.t = SYNC_IMAGE | SYNC_IFRAME; return settings; } diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index 0be70d16b8a..cd433ef2c81 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, isPlainObject, isArray, replaceAuctionPrice, isFn, logError, deepClone } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index f52cd8de759..838487925c8 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, deepAccess, createTrackPixelHtml } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 4ab98a77717..dcb43eb5f22 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -209,14 +209,31 @@ const browsersList = [ const listOfSupportedBrowsers = ['Safari', 'Chrome', 'Firefox', 'Microsoft Edge']; -function bidRequestedHandler(args) { +export function bidRequestedHandler(args) { const envelopeSourceCookieValue = storage.getCookie('_lr_env_src_ats'); const envelopeSource = envelopeSourceCookieValue === 'true'; let requests; requests = args.bids.map(function(bid) { return { envelope_source: envelopeSource, - has_envelope: bid.userId ? !!bid.userId.idl_env : false, + has_envelope: (function() { + // Check userIdAsEids for Prebid v10.0+ compatibility + if (bid.userIdAsEids && Array.isArray(bid.userIdAsEids)) { + const liverampEid = bid.userIdAsEids.find(eid => + eid.source === 'liveramp.com' + ); + if (liverampEid && liverampEid.uids && liverampEid.uids.length > 0) { + return true; + } + } + + // Fallback for older versions (backward compatibility) + if (bid.userId && bid.userId.idl_env) { + return true; + } + + return false; + })(), bidder: bid.bidder, bid_id: bid.bidId, auction_id: args.auctionId, diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js index 14f78045ff4..17b7d9bd8df 100644 --- a/modules/axonixBidAdapter.js +++ b/modules/axonixBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {deepAccess, isArray, isEmpty, logError, replaceAuctionPrice, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; diff --git a/modules/bidResponseFilter/index.js b/modules/bidResponseFilter/index.js index 5b138965983..64026958bc6 100644 --- a/modules/bidResponseFilter/index.js +++ b/modules/bidResponseFilter/index.js @@ -6,6 +6,7 @@ export const MODULE_NAME = 'bidResponseFilter'; export const BID_CATEGORY_REJECTION_REASON = 'Category is not allowed'; export const BID_ADV_DOMAINS_REJECTION_REASON = 'Adv domain is not allowed'; export const BID_ATTR_REJECTION_REASON = 'Attr is not allowed'; +export const BID_MEDIA_TYPE_REJECTION_REASON = `Media type is not allowed`; let moduleConfig; let enabled = false; @@ -29,13 +30,20 @@ export function reset() { export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctionManager.index) { const {bcat = [], badv = []} = index.getOrtb2(bid) || {}; - const battr = index.getBidRequest(bid)?.ortb2Imp[bid.mediaType]?.battr || index.getAdUnit(bid)?.ortb2Imp[bid.mediaType]?.battr || []; + const bidRequest = index.getBidRequest(bid); + const battr = bidRequest?.ortb2Imp[bid.mediaType]?.battr || index.getAdUnit(bid)?.ortb2Imp[bid.mediaType]?.battr || []; const catConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.cat || {})}; const advConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.adv || {})}; const attrConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.attr || {})}; + const mediaTypesConfig = {enforce: true, blockUnknown: true, ...(moduleConfig?.mediaTypes || {})}; - const { primaryCatId, secondaryCatIds = [], advertiserDomains = [], attr: metaAttr } = bid.meta || {}; + const { + primaryCatId, secondaryCatIds = [], + advertiserDomains = [], + attr: metaAttr, + mediaType: metaMediaType, + } = bid.meta || {}; // checking if bid fulfills ortb2 fields rules if ((catConfig.enforce && bcat.some(category => [primaryCatId, ...secondaryCatIds].includes(category))) || @@ -47,6 +55,9 @@ export function addBidResponseHook(next, adUnitCode, bid, reject, index = auctio } else if ((attrConfig.enforce && battr.includes(metaAttr)) || (attrConfig.blockUnknown && !metaAttr)) { reject(BID_ATTR_REJECTION_REASON); + } else if ((mediaTypesConfig.enforce && !Object.keys(bidRequest?.mediaTypes || {}).includes(metaMediaType)) || + (mediaTypesConfig.blockUnknown && !metaMediaType)) { + reject(BID_MEDIA_TYPE_REJECTION_REASON); } else { return next(adUnitCode, bid, reject); } diff --git a/modules/bmtmBidAdapter.js b/modules/bmtmBidAdapter.js index b4639f38ab2..bc6a4415f97 100644 --- a/modules/bmtmBidAdapter.js +++ b/modules/bmtmBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, deepAccess, logWarn, deepSetValue, isPlainObject } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 7d5611b741c..ac1d2147f3b 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -64,13 +64,6 @@ export function setTimestamp() { TIMESTAMP = timestamp(); } -export function initAnalytics() { - getGlobal().enableAnalytics({ - provider: 'browsi', - options: {} - }) -} - export function sendPageviewEvent(eventType) { if (eventType === 'PAGEVIEW') { window.addEventListener('browsi_pageview', () => { @@ -426,7 +419,6 @@ function init(moduleConfig) { _moduleParams = moduleConfig.params; _moduleParams.siteKey = moduleConfig.params.siteKey || moduleConfig.params.sitekey; _moduleParams.pubKey = moduleConfig.params.pubKey || moduleConfig.params.pubkey; - initAnalytics(); setTimestamp(); if (_moduleParams && _moduleParams.siteKey && _moduleParams.pubKey && _moduleParams.url) { sendModuleInitEvent(); diff --git a/modules/cadent_aperture_mxBidAdapter.js b/modules/cadent_aperture_mxBidAdapter.js index 73ff9367fac..f13bda26102 100644 --- a/modules/cadent_aperture_mxBidAdapter.js +++ b/modules/cadent_aperture_mxBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { _each, deepAccess, getBidIdParameter, diff --git a/modules/carodaBidAdapter.js b/modules/carodaBidAdapter.js index 9c8975542eb..ab1fb8b606c 100644 --- a/modules/carodaBidAdapter.js +++ b/modules/carodaBidAdapter.js @@ -40,6 +40,7 @@ export const spec = { ); }, buildRequests: (validBidRequests, bidderRequest) => { + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one topUsableWindow.carodaPageViewId = topUsableWindow.carodaPageViewId || Math.floor(Math.random() * 1e9); const pageViewId = topUsableWindow.carodaPageViewId; const ortbCommon = getORTBCommon(bidderRequest); diff --git a/modules/chtnwBidAdapter.js b/modules/chtnwBidAdapter.js index 17f1efcc6ab..3aea0016679 100644 --- a/modules/chtnwBidAdapter.js +++ b/modules/chtnwBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, _each, diff --git a/modules/clickioBidAdapter.js b/modules/clickioBidAdapter.js new file mode 100644 index 00000000000..954888291c3 --- /dev/null +++ b/modules/clickioBidAdapter.js @@ -0,0 +1,72 @@ +import {deepSetValue} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'clickio'; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.params', bidRequest.params); + return imp; + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests, bidderRequest}) + return [{ + method: 'POST', + url: 'https://o.clickiocdn.com/bids', + data + }] + }, + isBidRequestValid(bid) { + return true; + }, + interpretResponse(response, request) { + const bids = converter.fromORTB({response: response.body, request: request.data}).bids; + return bids; + }, + getUserSyncs(syncOptions, _, gdprConsent, uspConsent, gppConsent = {}) { + const { gppString = '', applicableSections = [] } = gppConsent; + const queryParams = []; + + if (gdprConsent) { + if (gdprConsent.gdprApplies !== undefined) { + queryParams.push(`gdpr=${gdprConsent.gdprApplies ? 1 : 0}`); + } + if (gdprConsent.consentString) { + queryParams.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + if (uspConsent) { + queryParams.push(`us_privacy=${uspConsent}`); + } + queryParams.push(`gpp=${gppString}`); + if (Array.isArray(applicableSections)) { + for (const applicableSection of applicableSections) { + queryParams.push(`gpp_sid=${applicableSection}`); + } + } + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `https://o.clickiocdn.com/cookie_sync_html?${queryParams.join('&')}` + } + ]; + } else { + return []; + } + } +}; + +registerBidder(spec); diff --git a/modules/clickioBidAdapter.md b/modules/clickioBidAdapter.md new file mode 100644 index 00000000000..5d3d41488ff --- /dev/null +++ b/modules/clickioBidAdapter.md @@ -0,0 +1,53 @@ +--- +layout: bidder +title: Clickio +description: Clickio Bidder Adapter +biddercode: clickio +media_types: banner +gdpr_supported: true +usp_supported: true +gpp_supported: true +schain_supported: true +coppa_supported: true +userId: all +--- + +# Overview + +``` +Module Name: Clickio Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@clickio.com +``` + +### Description + +The Clickio bid adapter connects to Clickio's demand platform using OpenRTB 2.5 standard. This adapter supports banner advertising. + +The Clickio bidding adapter requires initial setup before use. Please contact us at [support@clickio.com](mailto:support@clickio.com). +To get started, simply replace the ``said`` with the ID assigned to you. + +### Test Parameters + +```javascript +var adUnits = [ + { + code: 'clickio-banner-ad', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bids: [ + { + bidder: 'clickio', + params: { + said: 'test', + } + } + ] + } +]; +``` \ No newline at end of file diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index 4920a6cc974..d9394253497 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -3,6 +3,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; +import { getViewportSize } from '../libraries/viewport/viewport.js' +import { getDNT } from '../libraries/dnt/index.js' /** * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest @@ -12,7 +14,7 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.j */ const BIDDER_CODE = 'cointraffic'; -const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps.adsgravity.io/v1/request/prebid'; const DEFAULT_CURRENCY = 'EUR'; const ALLOWED_CURRENCIES = [ 'EUR', 'USD', 'JPY', 'BGN', 'CZK', 'DKK', 'GBP', 'HUF', 'PLN', 'RON', 'SEK', 'CHF', 'ISK', 'NOK', 'HRK', 'RUB', 'TRY', @@ -43,11 +45,24 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { - const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); - const currency = - config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || - getCurrencyFromBidderRequest(bidderRequest) || - DEFAULT_CURRENCY; + const sizes = parseSizesInput(bidRequest.params.size || bidRequest.mediaTypes.banner.sizes); + const { width, height } = getViewportSize(); + + const getCurrency = () => { + return config.getConfig(`currency.bidderCurrencyDefault.${BIDDER_CODE}`) || + getCurrencyFromBidderRequest(bidderRequest) || + DEFAULT_CURRENCY; + } + + const getLanguage = () => { + return navigator && navigator.language + ? navigator.language.indexOf('-') !== -1 + ? navigator.language.split('-')[0] + : navigator.language + : ''; + } + + const currency = getCurrency(); if (ALLOWED_CURRENCIES.indexOf(currency) === -1) { logError('Currency is not supported - ' + currency); @@ -60,6 +75,13 @@ export const spec = { sizes: sizes, bidId: bidRequest.bidId, referer: bidderRequest.refererInfo.ref, + device: { + width: width, + height: height, + user_agent: bidRequest.params.ua || navigator.userAgent, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, }; return { diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 01b7e91961a..006cb06d005 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -9,7 +9,7 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE} from '../src/storageManager.js'; import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -45,15 +45,23 @@ const O_AND_O_DOMAINS = [ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** + * Stores the ConnectID object in browser storage according to storage configuration * @function - * @param {Object} obj + * @param {Object} obj - The ID object to store + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' */ -function storeObject(obj) { +function storeObject(obj, storageConfig = {}) { const expires = Date.now() + STORAGE_DURATION; - if (storage.cookiesAreEnabled()) { + const storageType = storageConfig.type || ''; + + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + const useLocalStorage = !storageType || storageType.includes(STORAGE_TYPE_LOCALSTORAGE); + + if (useCookie && storage.cookiesAreEnabled()) { setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname()); } - if (storage.localStorageIsEnabled()) { + if (useLocalStorage && storage.localStorageIsEnabled()) { storage.setDataInLocalStorage(MODULE_NAME, JSON.stringify(obj)); } } @@ -110,8 +118,17 @@ function getIdFromLocalStorage() { return null; } -function syncLocalStorageToCookie() { - if (!storage.cookiesAreEnabled()) { +/** + * Syncs ID from localStorage to cookie if storage configuration allows + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + */ +function syncLocalStorageToCookie(storageConfig = {}) { + const storageType = storageConfig.type || ''; + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + + if (!useCookie || !storage.cookiesAreEnabled()) { return; } const value = getIdFromLocalStorage(); @@ -129,12 +146,19 @@ function isStale(storedIdData) { return false; } -function getStoredId() { +/** + * Retrieves stored ConnectID from cookie or localStorage + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + * @returns {Object|null} The stored ID object or null if not found + */ +function getStoredId(storageConfig = {}) { let storedId = getIdFromCookie(); if (!storedId) { storedId = getIdFromLocalStorage(); if (storedId && !isStale(storedId)) { - syncLocalStorageToCookie(); + syncLocalStorageToCookie(storageConfig); } } return storedId; @@ -191,13 +215,14 @@ export const connectIdSubmodule = { return; } const params = config.params || {}; + const storageConfig = config.storage || {}; if (!params || (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { logError(`${MODULE_NAME} module: configuration requires the 'pixelId'.`); return; } - const storedId = getStoredId(); + const storedId = getStoredId(storageConfig); let shouldResync = isStale(storedId); @@ -213,7 +238,7 @@ export const connectIdSubmodule = { } if (!shouldResync) { storedId.lastUsed = Date.now(); - storeObject(storedId); + storeObject(storedId, storageConfig); return {id: storedId}; } } @@ -274,7 +299,7 @@ export const connectIdSubmodule = { } responseObj.ttl = validTTLMiliseconds; } - storeObject(responseObj); + storeObject(responseObj, storageConfig); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); } diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index d805568e232..464499b5f7d 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, deepSetValue, mergeDeep, logWarn, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' diff --git a/modules/consentManagementGpp.ts b/modules/consentManagementGpp.ts index 905cffda213..fcbcaeb664c 100644 --- a/modules/consentManagementGpp.ts +++ b/modules/consentManagementGpp.ts @@ -11,10 +11,14 @@ import {enrichFPD} from '../src/fpd/enrichment.js'; import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js'; import {PbPromise, defer} from '../src/utils/promise.js'; import {type CMConfig, configParser} from '../libraries/consentManagement/cmUtils.js'; +import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js'; import {CONSENT_GPP} from "../src/consentHandler.ts"; export let consentConfig = {} as any; +// CMP event manager instance for GPP +let gppCmpEventManager: CmpEventManager | null = null; + type RelevantCMPData = { applicableSections: number[] gppString: string; @@ -101,6 +105,13 @@ export class GPPClient { logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`); } this.initialized = true; + + // Initialize CMP event manager and set CMP API + if (!gppCmpEventManager) { + gppCmpEventManager = createCmpEventManager('gpp'); + } + gppCmpEventManager.setCmpApi(this.cmp); + this.cmp({ command: 'addEventListener', callback: (event, success) => { @@ -120,6 +131,10 @@ export class GPPClient { if (gppDataHandler.getConsentData() != null && event?.pingData != null && !this.isCMPReady(event.pingData)) { gppDataHandler.setConsentData(null); } + + if (event?.listenerId !== null && event?.listenerId !== undefined) { + gppCmpEventManager?.setCmpListenerId(event?.listenerId); + } } }); } @@ -218,13 +233,23 @@ export function resetConsentData() { GPPClient.INST = null; } +export function removeCmpListener() { + // Clean up CMP event listeners before resetting + if (gppCmpEventManager) { + gppCmpEventManager.removeCmpEventListener(); + gppCmpEventManager = null; + } + resetConsentData(); +} + const parseConfig = configParser({ namespace: 'gpp', displayName: 'GPP', consentDataHandler: gppDataHandler, parseConsentData, getNullConsent: () => toConsentData(null), - cmpHandlers: cmpCallMap + cmpHandlers: cmpCallMap, + cmpEventCleanup: removeCmpListener }); export function setConsentConfig(config) { diff --git a/modules/consentManagementTcf.ts b/modules/consentManagementTcf.ts index e693132c8af..673a2d6f269 100644 --- a/modules/consentManagementTcf.ts +++ b/modules/consentManagementTcf.ts @@ -11,6 +11,7 @@ import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; import {cmpClient} from '../libraries/cmp/cmpClient.js'; import {configParser} from '../libraries/consentManagement/cmUtils.js'; +import {createCmpEventManager, type CmpEventManager} from '../libraries/cmp/cmpEventUtils.js'; import {CONSENT_GDPR} from "../src/consentHandler.ts"; import type {CMConfig} from "../libraries/consentManagement/cmUtils.ts"; @@ -24,6 +25,9 @@ const cmpCallMap = { 'iab': lookupIabConsent, }; +// CMP event manager instance for TCF +export let tcfCmpEventManager: CmpEventManager | null = null; + /** * @see https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework * @see https://github.com/InteractiveAdvertisingBureau/iabtcf-es/tree/master/modules/core#iabtcfcore @@ -87,6 +91,9 @@ function lookupIabConsent(setProvisionalConsent) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { try { + if (tcfData.listenerId !== null && tcfData.listenerId !== undefined) { + tcfCmpEventManager?.setCmpListenerId(tcfData.listenerId); + } gdprDataHandler.setConsentData(parseConsentData(tcfData)); resolve(); } catch (e) { @@ -113,6 +120,12 @@ function lookupIabConsent(setProvisionalConsent) { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); } + // Initialize CMP event manager and set CMP API + if (!tcfCmpEventManager) { + tcfCmpEventManager = createCmpEventManager('tcf', () => gdprDataHandler.getConsentData()); + } + tcfCmpEventManager.setCmpApi(cmp); + cmp({ command: 'addEventListener', callback: cmpResponseCallback @@ -159,14 +172,25 @@ export function resetConsentData() { gdprDataHandler.reset(); } +export function removeCmpListener() { + // Clean up CMP event listeners before resetting + if (tcfCmpEventManager) { + tcfCmpEventManager.removeCmpEventListener(); + tcfCmpEventManager = null; + } + resetConsentData(); +} + const parseConfig = configParser({ namespace: 'gdpr', displayName: 'TCF', consentDataHandler: gdprDataHandler, cmpHandlers: cmpCallMap, parseConsentData, - getNullConsent: () => toConsentData(null) + getNullConsent: () => toConsentData(null), + cmpEventCleanup: removeCmpListener } as any) + /** * A configuration function that initializes some module variables, as well as add a hook into the requestBids function */ diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 3c1bea6cc89..cbb3d047e0b 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -25,16 +25,19 @@ export const spec = { buildRequests: function(bidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - const bidRequest = bidRequests[0]; + const bidRequest = bidRequests[0] || {}; const tags = bidRequests.map(bidToTag); - const schain = bidRequest?.ortb2?.source?.ext?.schain; + const schain = bidRequest.ortb2?.source?.ext?.schain; const payload = { tags: [...tags], ua: navigator.userAgent, sdk: { - version: '$prebid.version$' + version: '$prebid.version$', + }, + schain: schain, + user: { + eids: bidRequest.userIdAsEids, }, - schain: schain }; if (bidderRequest) { if (bidderRequest.gdprConsent) { diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index a656dee0fc1..875dfdedf67 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,7 +2,6 @@ import { registerBidder } from "../src/adapters/bidderFactory.js"; import { getStorageManager } from "../src/storageManager.js"; import { BANNER } from "../src/mediaTypes.js"; import { - generateUUID, getParameterByName, isNumber, logError, @@ -27,11 +26,6 @@ export const BID_ENDPOINT = "https://prebid.cwi.re/v1/bid"; export const EVENT_ENDPOINT = "https://prebid.cwi.re/v1/event"; export const GVL_ID = 1081; -/** - * Allows limiting ad impressions per site render. Unique per prebid instance ID. - */ -export const pageViewId = generateUUID(); - export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); /** @@ -248,7 +242,7 @@ export const spec = { slots: processed, httpRef: referrer, // TODO: Verify whether the auctionId and the usage of pageViewId make sense. - pageViewId: pageViewId, + pageViewId: bidderRequest.pageViewId, networkBandwidth: getConnectionDownLink(window.navigator), sdk: { version: "$prebid.version$", diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 27e91028427..60ad1ffbcea 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -6,7 +6,6 @@ import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; -import {getExtraWinDimensions} from '../libraries/extraWinDimensions/extraWinDimensions.js'; import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); @@ -206,13 +205,12 @@ export const spec = { const botTest = new BotClientTests(); const win = getWindowTop(); const windowDimensions = getWinDimensions(); - const extraDims = getExtraWinDimensions(); return { 'wiw': windowDimensions.innerWidth, 'wih': windowDimensions.innerHeight, - 'saw': extraDims.screen.availWidth, - 'sah': extraDims.screen.availHeight, - 'scd': extraDims.screen.colorDepth, + 'saw': windowDimensions.screen.availWidth, + 'sah': windowDimensions.screen.availHeight, + 'scd': windowDimensions.screen.colorDepth, 'sw': windowDimensions.screen.width, 'sh': windowDimensions.screen.height, 'whl': win.history.length, diff --git a/modules/datawrkzAnalyticsAdapter.js b/modules/datawrkzAnalyticsAdapter.js index 3a80b15f6cd..94c2f670a5e 100644 --- a/modules/datawrkzAnalyticsAdapter.js +++ b/modules/datawrkzAnalyticsAdapter.js @@ -5,6 +5,7 @@ import { logInfo, logError } from '../src/utils.js'; let ENDPOINT = 'https://prebid-api.highr.ai/analytics'; const auctions = {}; +const adapterConfig = {}; const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analyticsType: 'endpoint' }), { @@ -125,15 +126,7 @@ const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analytic failureMessage: null, } - try { - fetch(ENDPOINT, { - method: 'POST', - body: JSON.stringify(payload), - headers: { 'Content-Type': 'application/json' } - }); - } catch (e) { - logError('[DatawrkzAnalytics] Failed to send AD_RENDER_SUCCEEDED event', e, payload); - } + this.sendToEndPoint(payload) break; } @@ -157,15 +150,7 @@ const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analytic failureMessage: message } - try { - fetch(ENDPOINT, { - method: 'POST', - body: JSON.stringify(payload), - headers: { 'Content-Type': 'application/json' } - }); - } catch (e) { - logError('[DatawrkzAnalytics] Failed to send AD_RENDER_FAILED event', e, payload); - } + this.sendToEndPoint(payload) break; } @@ -189,15 +174,7 @@ const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analytic adunits: adunitsArray }; - try { - fetch(ENDPOINT, { - method: 'POST', - body: JSON.stringify(payload), - headers: { 'Content-Type': 'application/json' } - }); - } catch (e) { - logError('[DatawrkzAnalytics] Sending failed', e, payload); - } + this.sendToEndPoint(payload) delete auctions[auctionId]; }, 2000); // Wait 2 seconds for BID_WON to happen @@ -208,6 +185,25 @@ const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analytic default: break; } + }, + sendToEndPoint(payload) { + if (!adapterConfig.publisherId || !adapterConfig.apiKey) { + logError('[DatawrkzAnalytics] Missing mandatory config: publisherId or apiKey. Skipping event.'); + return; + } + + payload.publisherId = adapterConfig.publisherId + payload.apiKey = adapterConfig.apiKey + + try { + fetch(ENDPOINT, { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'Content-Type': 'application/json' } + }); + } catch (e) { + logError('[DatawrkzAnalytics] Failed to send event', e, payload); + } } } ); @@ -215,6 +211,7 @@ const datawrkzAnalyticsAdapter = Object.assign(adapter({ url: ENDPOINT, analytic datawrkzAnalyticsAdapter.originEnableAnalytics = datawrkzAnalyticsAdapter.enableAnalytics; datawrkzAnalyticsAdapter.enableAnalytics = function (config) { + Object.assign(adapterConfig, config?.options || {}); datawrkzAnalyticsAdapter.originEnableAnalytics(config); logInfo('[DatawrkzAnalytics] Enabled with config:', config); }; diff --git a/modules/datawrkzAnalyticsAdapter.md b/modules/datawrkzAnalyticsAdapter.md index b4f7185d9be..d944b656038 100644 --- a/modules/datawrkzAnalyticsAdapter.md +++ b/modules/datawrkzAnalyticsAdapter.md @@ -19,5 +19,10 @@ Enable the adapter using: ```js pbjs.enableAnalytics({ - provider: 'datawrkzanalytics' + provider: 'datawrkzanalytics', + options: { + publisherId: 'YOUR_PUBLISHER_ID', + apiKey: 'YOUR_API_KEY' + } }); +``` diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index 26b9320af47..c5acc942218 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, deepSetValue, deepAccess, isArray, isFn, isPlainObject, logError, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; diff --git a/modules/defineMediaBidAdapter.js b/modules/defineMediaBidAdapter.js new file mode 100644 index 00000000000..937ad9fb8d8 --- /dev/null +++ b/modules/defineMediaBidAdapter.js @@ -0,0 +1,280 @@ +/** + * Define Media Bid Adapter for Prebid.js + * + * This adapter connects publishers to Define Media's programmatic advertising platform + * via OpenRTB 2.5 protocol. It supports banner ad formats and includes proper + * supply chain transparency through sellers.json compliance. + * + * @module defineMediaBidAdapter + * @version 1.0.0 + */ + +import {logInfo, logError, logWarn } from "../src/utils.js"; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import { ajax } from '../src/ajax.js'; + +// Bidder identification and compliance constants +const BIDDER_CODE = 'defineMedia'; +const IAB_GVL_ID = 440; // IAB Global Vendor List ID for GDPR compliance +const SUPPORTED_MEDIA_TYPES = [BANNER]; // Currently only banner ads are supported + +// Default bid response configuration +const DEFAULT_TTL = 1000; // Default time-to-live for bids in seconds +const DEFAULT_NET_REVENUE = true; // Revenue is reported as net (after platform fees) + +// Endpoint URLs for different environments +const ENDPOINT_URL_DEV = 'https://rtb-dev.conative.network/openrtb2/auction'; // Development/testing endpoint +const ENDPOINT_URL_PROD = 'https://rtb.conative.network/openrtb2/auction'; // Production endpoint +const METHOD = 'POST'; // HTTP method for bid requests + +/** + * Default ORTB converter instance with standard configuration + * This handles the conversion between Prebid.js bid objects and OpenRTB format + */ +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: IAB_GVL_ID, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + + /** + * Determines if a bid request is valid for this adapter + * + * Required parameters: + * - supplierDomainName: Domain name for supply chain transparency + * - mediaTypes.banner: Must include banner media type configuration + * + * Optional parameters: + * - devMode: Boolean flag to use development endpoint + * - ttl: Custom time-to-live for the bid response (only honored when devMode is true) + * + * @param {Object} bid - The bid request object from Prebid.js + * @returns {boolean} True if the bid request is valid + */ + isBidRequestValid: (bid) => { + // Ensure we have a valid bid object + if (!bid || typeof bid !== 'object') { + logInfo(`[${BIDDER_CODE}] isBidRequestValid: Invalid bid object`); + return false; + } + + // Validate required parameters + const hasSupplierDomainName = Boolean(bid?.params?.supplierDomainName); + const hasValidMediaType = Boolean(bid?.mediaTypes && bid.mediaTypes.banner); + const isDevMode = Boolean(bid?.params?.devMode); + + logInfo(`[${BIDDER_CODE}] isBidRequestValid called with:`, { + bidId: bid.bidId, + hasSupplierDomainName, + hasValidMediaType, + isDevMode + }); + + const isValid = hasSupplierDomainName && hasValidMediaType; + logInfo(`[${BIDDER_CODE}] isBidRequestValid returned:`, isValid); + return isValid; + }, + + /** + * Builds OpenRTB bid requests from validated Prebid.js bid requests + * + * This method: + * 1. Creates individual OpenRTB requests for each valid bid + * 2. Sets up dynamic TTL based on bid parameters (only in devMode) + * 3. Configures supply chain transparency (schain) + * 4. Selects appropriate endpoint based on devMode flag + * + * @param {Array} validBidRequests - Array of valid bid request objects + * @param {Object} bidderRequest - Bidder-level request data from Prebid.js + * @returns {Array} Array of bid request objects to send to the server + */ + buildRequests: (validBidRequests, bidderRequest) => { + return validBidRequests?.map(function(req) { + // DeepCopy the request to avoid modifying the original object + const oneBidRequest = [JSON.parse(JSON.stringify(req))]; + + // Get parameters and check devMode first + const params = oneBidRequest[0].params; + const isDevMode = Boolean(params?.devMode); + + // Custom TTL is only allowed in development mode for security and consistency + const ttl = isDevMode && params?.ttl ? params.ttl : DEFAULT_TTL; + + // Create converter with TTL (custom only in devMode, otherwise default) + const dynamicConverter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: ttl + } + }); + + // Convert Prebid.js request to OpenRTB format + const ortbRequest = dynamicConverter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: oneBidRequest + }); + + // Select endpoint based on development mode flag + const endpointUrl = isDevMode ? ENDPOINT_URL_DEV : ENDPOINT_URL_PROD; + + // Configure supply chain transparency (sellers.json compliance) + // Preserve existing schain if present, otherwise create minimal schain + if (bidderRequest?.source?.schain) { + // Preserve existing schain structure from bidderRequest + ortbRequest.source = bidderRequest.source; + } else { + // Create minimal schain only if none exists + if (!ortbRequest.source) { + ortbRequest.source = {}; + } + if (!ortbRequest.source.schain) { + ortbRequest.source.schain = { + complete: 1, // Indicates this is a complete supply chain + nodes: [{ + asi: params.supplierDomainName // Advertising system identifier + }] + }; + } + } + + logInfo(`[${BIDDER_CODE}] Mapped ORTB Request from`, oneBidRequest, ' to ', ortbRequest, ' with bidderRequest ', bidderRequest); + + return { + method: METHOD, + url: endpointUrl, + data: ortbRequest, + converter: dynamicConverter // Attach converter for response processing + } + }); + }, + + /** + * Processes bid responses from the Define Media server + * + * This method: + * 1. Validates the server response structure + * 2. Uses the appropriate ORTB converter (request-specific or default) + * 3. Converts OpenRTB response back to Prebid.js bid format + * 4. Handles errors gracefully and returns empty array on failure + * + * @param {Object} serverResponse - Response from the bid server + * @param {Object} request - Original request object containing converter + * @returns {Array} Array of bid objects for Prebid.js + */ + interpretResponse: (serverResponse, request) => { + logInfo(`[${BIDDER_CODE}] interpretResponse called with:`, { serverResponse, request }); + + // Validate server response structure + if (!serverResponse?.body) { + logWarn(`[${BIDDER_CODE}] No response body received`); + return []; + } + + try { + // Use the converter from the request if available (with custom TTL), otherwise use default + const responseConverter = request.converter || converter; + const bids = responseConverter.fromORTB({response: serverResponse.body, request: request.data}).bids; + logInfo(`[${BIDDER_CODE}] Successfully parsed ${bids.length} bids`); + return bids; + } catch (error) { + logError(`[${BIDDER_CODE}] Error parsing response:`, error); + return []; + } + }, + + /** + * Handles bid request timeouts + * Currently logs timeout events for monitoring and debugging + * + * @param {Array|Object} timeoutData - Timeout data from Prebid.js + */ + onTimeout: (timeoutData) => { + logInfo(`[${BIDDER_CODE}] onTimeout called with:`, timeoutData); + }, + + /** + * Handles successful bid wins + * + * This method: + * 1. Fires win notification URL (burl) if present in bid + * 2. Logs win event for analytics and debugging + * + * @param {Object} bid - The winning bid object + */ + onBidWon: (bid) => { + // Fire win notification URL for server-side tracking + if (bid?.burl) { + ajax(bid.burl, null, null); + } + logInfo(`[${BIDDER_CODE}] onBidWon called with bid:`, bid); + }, + + /** + * Handles bidder errors with comprehensive error categorization + * + * This method: + * 1. Categorizes errors by type (timeout, network, client/server errors) + * 2. Collects relevant context for debugging + * 3. Logs structured error information for monitoring + * + * Error categories: + * - timeout: Request exceeded time limit + * - network: Network connectivity issues + * - client_error: 4xx HTTP status codes + * - server_error: 5xx HTTP status codes + * - unknown: Uncategorized errors + * + * @param {Object} params - Error parameters + * @param {Object} params.error - Error object + * @param {Object} params.bidderRequest - Original bidder request + */ + onBidderError: ({ error, bidderRequest }) => { + // Collect comprehensive error information for debugging + const errorInfo = { + message: error?.message || 'Unknown error', + type: error?.type || 'general', + code: error?.code || null, + bidderCode: BIDDER_CODE, + auctionId: bidderRequest?.auctionId || 'unknown', + bidderRequestId: bidderRequest?.bidderRequestId || 'unknown', + timeout: bidderRequest?.timeout || null, + bids: bidderRequest?.bids?.length || 0 + }; + + // Categorize error types for better debugging and monitoring + if (error?.message?.includes('timeout')) { + errorInfo.category = 'timeout'; + } else if (error?.message?.includes('network')) { + errorInfo.category = 'network'; + } else if (error?.code >= 400 && error?.code < 500) { + errorInfo.category = 'client_error'; + } else if (error?.code >= 500) { + errorInfo.category = 'server_error'; + } else { + errorInfo.category = 'unknown'; + } + + logError(`[${BIDDER_CODE}] Bidder error occurred:`, errorInfo); + }, + + /** + * Handles successful ad rendering events + * Currently logs render success for analytics and debugging + * + * @param {Object} bid - The successfully rendered bid object + */ + onAdRenderSucceeded: (bid) => { + logInfo(`[${BIDDER_CODE}] onAdRenderSucceeded called with bid:`, bid); + } +}; + +// Register the bidder with Prebid.js +registerBidder(spec); diff --git a/modules/defineMediaBidAdapter.md b/modules/defineMediaBidAdapter.md new file mode 100644 index 00000000000..7930446e305 --- /dev/null +++ b/modules/defineMediaBidAdapter.md @@ -0,0 +1,49 @@ +# Overview + +``` +Module Name: Define Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: m.klumpp@definemedia.de +``` + +# Description + +This is the official Define Media Bid Adapter for Prebid.js. It currently supports **Banner**. Delivery is handled by Define Media’s own RTB server. +Publishers are onboarded and activated via Define Media **Account Management** (no self-service keys required). + +# Bid Parameters + +| Name | Scope | Type | Description | Example | +|---------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| `supplierDomainName`| required | string | **Identifier used for the supply chain (schain)**. Populates `source.schain.nodes[0].asi` to attribute traffic to Define Media’s supply path. **Publishers do not need to host a sellers.json under this domain.** | `definemedia.de` | +| `devMode` | optional | boolean | Sends requests to the development endpoint. Requests with `devMode: true` are **not billable**. | `true` | + + +# How it works + +- The adapter converts Prebid bid requests to ORTB and sets: + - `source.schain.complete = 1` + - `source.schain.nodes[0].asi = supplierDomainName` +- This ensures buyers can resolve the **supply chain** correctly without requiring any sellers.json hosted by the publisher. + +# Example Prebid Configuration + +```js +pbjs.addAdUnits([{ + code: 'div-gpt-ad-123', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bids: [{ + bidder: 'defineMedia', + params: { + supplierDomainName: 'definemedia.de', + // set only for non-billable tests + devMode: false + } + }] +}]); +``` + +# Notes + +- **Onboarding**: Publishers must be enabled by Define Media Account Management before traffic is accepted. +- **Transparency**: Seller transparency is enforced on Define Media’s side via account setup and standard industry mechanisms (e.g., schain). No publisher-hosted sellers.json is expected or required. diff --git a/modules/digitalMatterBidAdapter.js b/modules/digitalMatterBidAdapter.js index ce329bca929..37c01e6a9d9 100644 --- a/modules/digitalMatterBidAdapter.js +++ b/modules/digitalMatterBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {deepAccess, deepSetValue, getWinDimensions, inIframe, logWarn, parseSizesInput} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 4ef15f995f2..78d5c143f55 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; diff --git a/modules/distroscaleBidAdapter.js b/modules/distroscaleBidAdapter.js index 547dd254a5f..4737c8bf969 100644 --- a/modules/distroscaleBidAdapter.js +++ b/modules/distroscaleBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { logWarn, isPlainObject, isStr, isArray, isFn, inIframe, mergeDeep, deepSetValue, logError, deepClone } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; diff --git a/modules/empowerBidAdapter.js b/modules/empowerBidAdapter.js new file mode 100644 index 00000000000..14e8be2a82d --- /dev/null +++ b/modules/empowerBidAdapter.js @@ -0,0 +1,262 @@ +import { + deepAccess, + mergeDeep, + logError, + replaceMacros, + triggerPixel, + deepSetValue, + isStr, + isArray, + getWinDimensions, +} from "../src/utils.js"; +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { config } from "../src/config.js"; +import { VIDEO, BANNER } from "../src/mediaTypes.js"; +import { getConnectionType } from "../libraries/connectionInfo/connectionUtils.js"; + +export const ENDPOINT = "https://bid.virgul.com/prebid"; + +const BIDDER_CODE = "empower"; +const GVLID = 1248; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [VIDEO, BANNER], + + isBidRequestValid: (bid) => + !!(bid && bid.params && bid.params.zone && bid.bidder === BIDDER_CODE), + + buildRequests: (bidRequests, bidderRequest) => { + const currencyObj = config.getConfig("currency"); + const currency = (currencyObj && currencyObj.adServerCurrency) || "USD"; + + const request = { + id: bidRequests[0].bidderRequestId, + at: 1, + imp: bidRequests.map((slot) => impression(slot, currency)), + site: { + page: bidderRequest.refererInfo.page, + domain: bidderRequest.refererInfo.domain, + ref: bidderRequest.refererInfo.ref, + publisher: { domain: bidderRequest.refererInfo.domain }, + }, + device: { + ua: navigator.userAgent, + js: 1, + dnt: + navigator.doNotTrack === "yes" || + navigator.doNotTrack === "1" || + navigator.msDoNotTrack === "1" + ? 1 + : 0, + h: screen.height, + w: screen.width, + language: navigator.language, + connectiontype: getConnectionType(), + }, + cur: [currency], + source: { + fd: 1, + tid: bidderRequest.ortb2?.source?.tid, + ext: { + prebid: "$prebid.version$", + }, + }, + user: {}, + regs: {}, + ext: {}, + }; + + if (bidderRequest.gdprConsent) { + request.user = { + ext: { + consent: bidderRequest.gdprConsent.consentString || "", + }, + }; + request.regs = { + ext: { + gdpr: + bidderRequest.gdprConsent.gdprApplies !== undefined + ? bidderRequest.gdprConsent.gdprApplies + : true, + }, + }; + } + + if (bidderRequest.ortb2?.source?.ext?.schain) { + request.schain = bidderRequest.ortb2.source.ext.schain; + } + + let bidUserIdAsEids = deepAccess(bidRequests, "0.userIdAsEids"); + if (isArray(bidUserIdAsEids) && bidUserIdAsEids.length > 0) { + deepSetValue(request, "user.eids", bidUserIdAsEids); + } + + const commonFpd = bidderRequest.ortb2 || {}; + const { user, device, site, bcat, badv } = commonFpd; + if (site) { + mergeDeep(request, { site: site }); + } + if (user) { + mergeDeep(request, { user: user }); + } + if (badv) { + mergeDeep(request, { badv: badv }); + } + if (bcat) { + mergeDeep(request, { bcat: bcat }); + } + + if (user?.geo && device?.geo) { + request.device.geo = { ...request.device.geo, ...device.geo }; + request.user.geo = { ...request.user.geo, ...user.geo }; + } else { + if (user?.geo || device?.geo) { + request.user.geo = request.device.geo = user?.geo + ? { ...request.user.geo, ...user.geo } + : { ...request.user.geo, ...device.geo }; + } + } + + if (bidderRequest.ortb2?.device) { + mergeDeep(request.device, bidderRequest.ortb2.device); + } + + return { + method: "POST", + url: ENDPOINT, + data: JSON.stringify(request), + }; + }, + + interpretResponse: (bidResponse, bidRequest) => { + const idToImpMap = {}; + const idToBidMap = {}; + + if (!bidResponse["body"]) { + return []; + } + if (!bidRequest.data) { + return []; + } + const requestImps = parse(bidRequest.data); + if (!requestImps) { + return []; + } + requestImps.imp.forEach((imp) => { + idToImpMap[imp.id] = imp; + }); + bidResponse = bidResponse.body; + if (bidResponse) { + bidResponse.seatbid.forEach((seatBid) => + seatBid.bid.forEach((bid) => { + idToBidMap[bid.impid] = bid; + }) + ); + } + const bids = []; + Object.keys(idToImpMap).forEach((id) => { + const imp = idToImpMap[id]; + const result = idToBidMap[id]; + + if (result) { + const bid = { + requestId: id, + cpm: result.price, + creativeId: result.crid, + ttl: 300, + netRevenue: true, + mediaType: imp.video ? VIDEO : BANNER, + currency: bidResponse.cur, + }; + if (imp.video) { + bid.vastXml = result.adm; + } else if (imp.banner) { + bid.ad = result.adm; + } + bid.width = result.w; + bid.height = result.h; + if (result.burl) bid.burl = result.burl; + if (result.nurl) bid.nurl = result.nurl; + if (result.adomain) { + bid.meta = { + advertiserDomains: result.adomain, + }; + } + bids.push(bid); + } + }); + return bids; + }, + + onBidWon: (bid) => { + if (bid.nurl && isStr(bid.nurl)) { + bid.nurl = replaceMacros(bid.nurl, { + AUCTION_PRICE: bid.cpm, + AUCTION_CURRENCY: bid.cur, + }); + triggerPixel(bid.nurl); + } + }, +}; + +function impression(slot, currency) { + let bidFloorFromModule; + if (typeof slot.getFloor === "function") { + const floorInfo = slot.getFloor({ + currency: "USD", + mediaType: "*", + size: "*", + }); + bidFloorFromModule = + floorInfo?.currency === "USD" ? floorInfo?.floor : undefined; + } + const imp = { + id: slot.bidId, + bidfloor: bidFloorFromModule || slot.params.bidfloor || 0, + bidfloorcur: + (bidFloorFromModule && "USD") || + slot.params.bidfloorcur || + currency || + "USD", + tagid: "" + (slot.params.zone || ""), + }; + + if (slot.mediaTypes.banner) { + imp.banner = bannerImpression(slot); + } else if (slot.mediaTypes.video) { + imp.video = deepAccess(slot, "mediaTypes.video"); + } + imp.ext = slot.params || {}; + const { innerWidth, innerHeight } = getWinDimensions(); + imp.ext.ww = innerWidth || ""; + imp.ext.wh = innerHeight || ""; + return imp; +} + +function bannerImpression(slot) { + const sizes = slot.mediaTypes.banner.sizes || slot.sizes; + return { + format: sizes.map((s) => ({ w: s[0], h: s[1] })), + w: sizes[0][0], + h: sizes[0][1], + }; +} + +function parse(rawResponse) { + try { + if (rawResponse) { + if (typeof rawResponse === "object") { + return rawResponse; + } else { + return JSON.parse(rawResponse); + } + } + } catch (ex) { + logError("empowerBidAdapter", "ERROR", ex); + } + return null; +} + +registerBidder(spec); diff --git a/modules/empowerBidAdapter.md b/modules/empowerBidAdapter.md new file mode 100644 index 00000000000..b627cd25282 --- /dev/null +++ b/modules/empowerBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +Module Name: Empower Bid Adapter + +Module Type: Bidder Adapter + +Maintainer: prebid@empower.net + +# Description + +Module that connects to Empower's demand sources + +This adapter requires setup and approval from Empower.net. +Please reach out to your account team or info@empower.net for more information. + +# Test Parameters +```javascript + var adUnits = [ + { + code: '/19968336/prebid_banner_example_1', + mediaTypes: { + banner: { + sizes: [[970, 250], [300, 250]], + } + }, + bids: [{ + bidder: 'empower', + params: { + bidfloor: 0.50, + zone: 123456, + site: 'example' + }, + }] + } + ]; +``` diff --git a/modules/fwsspBidAdapter.js b/modules/fwsspBidAdapter.js index c94ba8342d7..3a07402eb44 100644 --- a/modules/fwsspBidAdapter.js +++ b/modules/fwsspBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, logError, logWarn, isArray, isFn, deepAccess, formatQS } from '../src/utils.js'; +import { logInfo, logError, logWarn, isArray, isFn, deepAccess } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; @@ -6,7 +6,8 @@ import { getGlobal } from '../src/prebidGlobal.js'; const BIDDER_CODE = 'fwssp'; const GVL_ID = 285; -const USER_SYNC_URL = 'https://ads.stickyadstv.com/auto-user-sync'; +const USER_SYNC_URL = 'https://user-sync.fwmrm.net/ad/u?'; +let PRIVACY_VALUES = {}; export const spec = { code: BIDDER_CODE, @@ -21,7 +22,7 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid(bid) { - return !!(bid.params.serverUrl && bid.params.networkId && bid.params.profile && bid.params.siteSectionId && bid.params.videoAssetId); + return !!(bid.params.serverUrl && bid.params.networkId && bid.params.profile && bid.params.siteSectionId); }, /** @@ -46,9 +47,9 @@ export const spec = { const buildRequest = (currentBidRequest, bidderRequest) => { const globalParams = constructGlobalParams(currentBidRequest); const keyValues = constructKeyValues(currentBidRequest, bidderRequest); - const slotParams = constructSlotParams(currentBidRequest); - const dataString = constructDataString(globalParams, keyValues, slotParams); + const serializedSChain = constructSupplyChain(currentBidRequest, bidderRequest); + const dataString = constructDataString(globalParams, keyValues, serializedSChain, slotParams); return { method: 'GET', url: currentBidRequest.params.serverUrl, @@ -57,6 +58,19 @@ export const spec = { }; } + const constructSupplyChain = (currentBidRequest, bidderRequest) => { + // Add schain object + let schain = deepAccess(bidderRequest, 'ortb2.source.schain'); + if (!schain) { + schain = deepAccess(bidderRequest, 'ortb2.source.ext.schain'); + } + if (!schain) { + schain = currentBidRequest.schain; + } + + return this.serializeSupplyChain(schain) + } + const constructGlobalParams = currentBidRequest => { const sdkVersion = getSDKVersion(currentBidRequest); const prebidVersion = getGlobal().version; @@ -65,7 +79,7 @@ export const spec = { resp: 'vast4', prof: currentBidRequest.params.profile, csid: currentBidRequest.params.siteSectionId, - caid: currentBidRequest.params.videoAssetId, + caid: currentBidRequest.params.videoAssetId ? currentBidRequest.params.videoAssetId : 0, pvrn: getRandomNumber(), vprn: getRandomNumber(), flag: setFlagParameter(currentBidRequest.params.flags), @@ -127,23 +141,6 @@ export const spec = { } } - // Add schain object - let schain = deepAccess(bidderRequest, 'ortb2.source.schain'); - if (!schain) { - schain = deepAccess(bidderRequest, 'ortb2.source.ext.schain'); - } - if (!schain) { - schain = currentBidRequest.schain; - } - - if (schain) { - try { - keyValues.schain = JSON.stringify(schain); - } catch (error) { - logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the schain: ' + error); - } - } - // Add 3rd party user ID if (currentBidRequest.userIdAsEids && currentBidRequest.userIdAsEids.length > 0) { try { @@ -195,6 +192,34 @@ export const spec = { keyValues._fw_placement_type = videoPlacement; keyValues._fw_plcmt_type = videoPlcmt; } + + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa'); + if (typeof coppa === 'number') { + keyValues._fw_coppa = coppa; + } + + const atts = deepAccess(bidderRequest, 'ortb2.device.ext.atts'); + if (typeof atts === 'number') { + keyValues._fw_atts = atts; + } + + const lmt = deepAccess(bidderRequest, 'ortb2.device.lmt'); + if (typeof lmt === 'number') { + keyValues._fw_is_lat = lmt; + } + + PRIVACY_VALUES = {} + if (keyValues._fw_coppa != null) { + PRIVACY_VALUES._fw_coppa = keyValues._fw_coppa; + } + + if (keyValues._fw_atts != null) { + PRIVACY_VALUES._fw_atts = keyValues._fw_atts; + } + if (keyValues._fw_is_lat != null) { + PRIVACY_VALUES._fw_is_lat = keyValues._fw_is_lat; + } + return keyValues; } @@ -232,19 +257,10 @@ export const spec = { return slotParams } - const constructDataString = (globalParams, keyValues, slotParams) => { - // Helper function to append parameters to the data string and to not include the last '&' param before '; - const appendParams = (params) => { - const keys = Object.keys(params); - return keys.map((key, index) => { - const encodedKey = encodeURIComponent(key); - const encodedValue = encodeURIComponent(params[key]); - return `${encodedKey}=${encodedValue}${index < keys.length - 1 ? '&' : ''}`; - }).join(''); - }; - + const constructDataString = (globalParams, keyValues, serializedSChain, slotParams) => { const globalParamsString = appendParams(globalParams) + ';'; - const keyValuesString = appendParams(keyValues) + ';'; + // serializedSChain requires special encoding logic as outlined in the ORTB spec https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/supplychainobject.md + const keyValuesString = appendParams(keyValues) + serializedSChain + ';'; const slotParamsString = appendParams(slotParams) + ';'; return globalParamsString + keyValuesString + slotParamsString; @@ -255,6 +271,21 @@ export const spec = { }); }, + /** + * Serialize a supply chain object to a string uri encoded + * + * @param {*} schain object + */ + serializeSupplyChain: function(schain) { + if (!schain || !schain.nodes) return ''; + const nodesProperties = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + return `&schain=${schain.ver},${schain.complete}!` + + schain.nodes.map(node => nodesProperties.map(prop => + node[prop] ? encodeURIComponent(node[prop]) : '') + .join(',')) + .join('!'); + }, + /** * Unpack the response from the server into a list of bids. * @@ -334,7 +365,10 @@ export const spec = { }, getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent, gppConsent) { - const params = {}; + const params = { + mode: 'auto-user-sync', + ...PRIVACY_VALUES + }; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { @@ -358,21 +392,19 @@ export const spec = { } } - let queryString = ''; - if (params) { - queryString = '?' + `${formatQS(params)}`; - } + const url = USER_SYNC_URL + appendParams(params); const syncs = []; if (syncOptions && syncOptions.pixelEnabled) { syncs.push({ type: 'image', - url: USER_SYNC_URL + queryString + url: url }); - } else if (syncOptions.iframeEnabled) { + } + if (syncOptions && syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: USER_SYNC_URL + queryString + url: url }); } @@ -728,4 +760,14 @@ function getTopMostWindow() { return res; } +// Helper function to append parameters to the data string and to not include the last '&' param before '; +function appendParams(params) { + const keys = Object.keys(params); + return keys.map((key, index) => { + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(params[key]); + return `${encodedKey}=${encodedValue}${index < keys.length - 1 ? '&' : ''}`; + }).join(''); +} + registerBidder(spec); diff --git a/modules/fwsspBidAdapter.md b/modules/fwsspBidAdapter.md index 498897b676a..fb44dd279b2 100644 --- a/modules/fwsspBidAdapter.md +++ b/modules/fwsspBidAdapter.md @@ -8,6 +8,37 @@ Maintainer: vis@freewheel.com Module that connects to Freewheel MRM's demand sources +# Basic Test Request +``` +const adUnits = [{ + code: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + minduration: 30, + maxduration: 60 + } + }, + bids: [{ + bidder: 'fwssp', // or use alias 'freewheel-mrm' + params: { + serverUrl: 'https://example.com/ad/g/1', + networkId: '42015', + profile: '42015:js_allinone_profile', + siteSectionId: 'js_allinone_demo_site_section', + videoAssetId: '1', // optional: default value of 0 will used if not included + flags: '+play-uapl' // optional: users may include capability if needed + mode: 'live', + adRequestKeyValues: { // optional: users may include adRequestKeyValues if needed + _fw_player_width: '1920', + _fw_player_height: '1080' + }, + format: 'inbanner' + } + }] +}]; +``` + # Example Inbanner Ad Request ``` { @@ -19,6 +50,17 @@ Module that connects to Freewheel MRM's demand sources }, bids: [{ bidder: 'fwssp', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + }] + }, params: { bidfloor: 2.00, serverUrl: 'https://example.com/ad/g/1', @@ -26,7 +68,7 @@ Module that connects to Freewheel MRM's demand sources profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', flags: '+play', - videoAssetId: '0', + videoAssetId: '1`, // optional: default value of 0 will used if not included timePosition: 120, adRequestKeyValues: { _fw_player_width: '1920', @@ -51,6 +93,17 @@ Module that connects to Freewheel MRM's demand sources }, bids: [{ bidder: 'fwssp', + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '0', + hp: 1, + rid: 'bidrequestid', + domain: 'example.com' + }] + }, params: { bidfloor: 2.00, serverUrl: 'https://example.com/ad/g/1', @@ -58,7 +111,7 @@ Module that connects to Freewheel MRM's demand sources profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', flags: '+play', - videoAssetId: '0', + videoAssetId: '1', // optional: default value of 0 will used if not included mode: 'live', timePosition: 120, tpos: 300, diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index c57a3be9b1a..7bff19bb2c0 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,6 +1,6 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.js'; import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js'; -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { diff --git a/modules/greenbidsBidAdapter.js b/modules/greenbidsBidAdapter.js index 2b5a0790459..490eb9e703d 100644 --- a/modules/greenbidsBidAdapter.js +++ b/modules/greenbidsBidAdapter.js @@ -10,7 +10,7 @@ import { getReferrerInfo, getPageTitle, getPageDescription, getConnectionDownLin */ const BIDDER_CODE = 'greenbids'; -const ENDPOINT_URL = 'https://hb.greenbids.ai'; +export const ENDPOINT_URL = 'https://hb.greenbids.ai'; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const spec = { diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index 540cd82257d..06e0e7a149e 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -43,7 +43,7 @@ export const spec = { ...buildStoredRequest(bid), }; - // GDPR: If available, include GDPR signals in the request + // GDPR if (bidderRequest && bidderRequest.gdprConsent) { deepSetValue( requestData, @@ -57,7 +57,7 @@ export const spec = { ); } - // GPP: If available, include GPP data in regs.ext + // GPP if (bidderRequest && bidderRequest.gpp) { deepSetValue(requestData, 'regs.ext.gpp', bidderRequest.gpp); } @@ -65,12 +65,12 @@ export const spec = { deepSetValue(requestData, 'regs.ext.gpp_sid', bidderRequest.gppSids); } - // US Privacy: If available, include US Privacy signal in regs.ext + // US Privacy if (bidderRequest && bidderRequest.usPrivacy) { deepSetValue(requestData, 'regs.ext.us_privacy', bidderRequest.usPrivacy); } - // If user IDs are available, add them under user.ext.eids + // User IDs if (bid.userIdAsEids) { deepSetValue(requestData, 'user.ext.eids', bid.userIdAsEids); } @@ -95,17 +95,26 @@ export const spec = { seatbid.bid.forEach((bid) => { const impId = bid.impid; // Unique identifier matching getImp(bid).id - // Build meta object with adomain and networkId, preserving any existing data + // --- MINIMAL CHANGE START --- + // Build meta object and propagate advertiser domains for hb_adomain const meta = deepAccess(bid, 'ext.prebid.meta', {}) || {}; - const adomain = deepAccess(bid, 'adomain', []); - if (adomain.length > 0) { - meta.adomain = adomain; + // Read ORTB adomain; normalize to array of clean strings + let advertiserDomains = deepAccess(bid, 'adomain', []); + advertiserDomains = Array.isArray(advertiserDomains) + ? advertiserDomains + .filter(Boolean) + .map(d => String(d).toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').trim()) + : []; + if (advertiserDomains.length > 0) { + meta.advertiserDomains = advertiserDomains; // <-- Prebid uses this to set hb_adomain } const networkId = deepAccess(bid, 'ext.prebid.meta.networkId'); if (networkId) { meta.networkId = networkId; } + // Keep writing back for completeness (preserves existing behavior) deepSetValue(bid, 'ext.prebid.meta', meta); + // --- MINIMAL CHANGE END --- const currentBidResponse = { requestId: impId, // Using imp.id as the unique request identifier @@ -117,7 +126,7 @@ export const spec = { currency: serverResponse.body.cur, netRevenue: true, ttl: TIME_TO_LIVE, - meta: meta, + meta: meta, // includes advertiserDomains now }; // For each imp, only keep the bid with the highest CPM diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index 5effa1bb463..d8f553c6ae0 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -570,36 +570,16 @@ function incrementNb(cachedObj) { function updateTargeting(fetchResponse, config) { if (config.params.gamTargetingPrefix) { - const tags = {}; - let universalUid = fetchResponse.universal_uid; - if (universalUid.startsWith('ID5*')) { - tags.id = "y"; - } - let abTestingResult = fetchResponse.ab_testing?.result; - switch (abTestingResult) { - case 'control': - tags.ab = 'c'; - break; - case 'normal': - tags.ab = 'n'; - break; - } - let enrichment = fetchResponse.enrichment; - if (enrichment?.enriched === true) { - tags.enrich = 'y'; - } else if (enrichment?.enrichment_selected === true) { - tags.enrich = 's'; - } else if (enrichment?.enrichment_selected === false) { - tags.enrich = 'c'; + const tags = fetchResponse.tags; + if (tags) { + window.googletag = window.googletag || {cmd: []}; + window.googletag.cmd = window.googletag.cmd || []; + window.googletag.cmd.push(() => { + for (const tag in tags) { + window.googletag.pubads().setTargeting(config.params.gamTargetingPrefix + '_' + tag, tags[tag]); + } + }); } - - window.googletag = window.googletag || {cmd: []}; - window.googletag.cmd = window.googletag.cmd || []; - window.googletag.cmd.push(() => { - for (const tag in tags) { - window.googletag.pubads().setTargeting(config.params.gamTargetingPrefix + '_' + tag, tags[tag]); - } - }); } } diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index 4b8dba3b685..9819a1587e9 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, deepSetValue, generateUUID, getWinDimensions, isPlainObject } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; diff --git a/modules/intentIqAnalyticsAdapter.js b/modules/intentIqAnalyticsAdapter.js index f1322d8a982..06c9bcb28b4 100644 --- a/modules/intentIqAnalyticsAdapter.js +++ b/modules/intentIqAnalyticsAdapter.js @@ -1,26 +1,39 @@ -import {logError, logInfo} from '../src/utils.js'; +import { isPlainObject, logError, logInfo } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import {ajax} from '../src/ajax.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {config} from '../src/config.js'; -import {EVENTS} from '../src/constants.js'; -import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; -import {appendSPData} from '../libraries/intentIqUtils/urlUtils.js'; -import {appendVrrefAndFui, getReferrer} from '../libraries/intentIqUtils/getRefferer.js'; -import {getCmpData} from '../libraries/intentIqUtils/getCmpData.js' -import {CLIENT_HINTS_KEY, FIRST_PARTY_KEY, VERSION, PREBID} from '../libraries/intentIqConstants/intentIqConstants.js'; -import {readData, defineStorageType} from '../libraries/intentIqUtils/storageUtils.js'; -import {reportingServerAddress} from '../libraries/intentIqUtils/intentIqConfig.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { config } from '../src/config.js'; +import { EVENTS } from '../src/constants.js'; +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js'; +import { detectBrowser } from '../libraries/intentIqUtils/detectBrowserUtils.js'; +import { appendSPData } from '../libraries/intentIqUtils/urlUtils.js'; +import { appendVrrefAndFui, getReferrer } from '../libraries/intentIqUtils/getRefferer.js'; +import { getCmpData } from '../libraries/intentIqUtils/getCmpData.js'; +import { + CLIENT_HINTS_KEY, + FIRST_PARTY_KEY, + VERSION, + PREBID +} from '../libraries/intentIqConstants/intentIqConstants.js'; +import { readData, defineStorageType } from '../libraries/intentIqUtils/storageUtils.js'; +import { reportingServerAddress } from '../libraries/intentIqUtils/intentIqConfig.js'; import { handleAdditionalParams } from '../libraries/intentIqUtils/handleAdditionalParams.js'; +import { gamPredictionReport } from '../libraries/intentIqUtils/gamPredictionReport.js'; -const MODULE_NAME = 'iiqAnalytics' +const MODULE_NAME = 'iiqAnalytics'; const analyticsType = 'endpoint'; -const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); +const storage = getStorageManager({ + moduleType: MODULE_TYPE_ANALYTICS, + moduleName: MODULE_NAME +}); const prebidVersion = '$prebid.version$'; export const REPORTER_ID = Date.now() + '_' + getRandom(0, 1000); const allowedStorage = defineStorageType(config.enabledStorageTypes); +let globalName; +let alreadySubscribedOnGAM = false; +let reportList = {}; +let cleanReportsID; const PARAMS_NAMES = { abTestGroup: 'abGroup', @@ -61,20 +74,19 @@ const PARAMS_NAMES = { }; function getIntentIqConfig() { - return config.getConfig('userSync.userIds')?.find(m => m.name === 'intentIqId'); + return config.getConfig('userSync.userIds')?.find((m) => m.name === 'intentIqId'); } -const DEFAULT_URL = 'https://reports.intentiq.com/report' +const DEFAULT_URL = 'https://reports.intentiq.com/report'; const getDataForDefineURL = () => { - const iiqConfig = getIntentIqConfig() const cmpData = getCmpData(); const gdprDetected = cmpData.gdprString; - return [iiqConfig, gdprDetected] -} + return [iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress, gdprDetected]; +}; -const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, analyticsType}), { +const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({ url: DEFAULT_URL, analyticsType }), { initOptions: { lsValueInitialized: false, partner: null, @@ -87,15 +99,22 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, an domainName: null, siloEnabled: false, reportMethod: null, - additionalParams: null + additionalParams: null, + reportingServerAddress: '' }, - track({eventType, args}) { + track({ eventType, args }) { switch (eventType) { case BID_WON: bidWon(args); break; case BID_REQUESTED: + checkAndInitConfig(); defineGlobalVariableName(); + if (!alreadySubscribedOnGAM && shouldSubscribeOnGAM()) { + alreadySubscribedOnGAM = true; + const iiqConfig = getIntentIqConfig(); + gamPredictionReport(iiqConfig?.params?.gamObjectReference, bidWon); + } break; default: break; @@ -104,29 +123,34 @@ const iiqAnalyticsAnalyticsAdapter = Object.assign(adapter({url: DEFAULT_URL, an }); // Events needed -const { - BID_WON, - BID_REQUESTED -} = EVENTS; +const { BID_WON, BID_REQUESTED } = EVENTS; -function initAdapterConfig() { +function initAdapterConfig(config) { if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) return; - const iiqConfig = getIntentIqConfig() + const iiqIdSystemConfig = getIntentIqConfig(); - if (iiqConfig) { + if (iiqIdSystemConfig) { + const { manualWinReportEnabled, gamPredictReporting, reportMethod, reportingServerAddress: reportEndpoint, adUnitConfig } = config?.options || {} iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = true; iiqAnalyticsAnalyticsAdapter.initOptions.partner = - iiqConfig.params?.partner && !isNaN(iiqConfig.params.partner) ? iiqConfig.params.partner : -1; + iiqIdSystemConfig.params?.partner && !isNaN(iiqIdSystemConfig.params.partner) ? iiqIdSystemConfig.params.partner : -1; iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList = - typeof iiqConfig.params?.browserBlackList === 'string' ? iiqConfig.params.browserBlackList.toLowerCase() : ''; - iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = iiqConfig.params?.manualWinReportEnabled || false; - iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqConfig.params?.domainName || ''; + typeof iiqIdSystemConfig.params?.browserBlackList === 'string' + ? iiqIdSystemConfig.params.browserBlackList.toLowerCase() + : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = + manualWinReportEnabled || false; + iiqAnalyticsAnalyticsAdapter.initOptions.domainName = iiqIdSystemConfig.params?.domainName || ''; iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled = - typeof iiqConfig.params?.siloEnabled === 'boolean' ? iiqConfig.params.siloEnabled : false; - iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(iiqConfig.params?.reportMethod); - iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = iiqConfig.params?.additionalParams || null; + typeof iiqIdSystemConfig.params?.siloEnabled === 'boolean' ? iiqIdSystemConfig.params.siloEnabled : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = parseReportingMethod(reportMethod); + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams = iiqIdSystemConfig.params?.additionalParams || null; + iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting = typeof gamPredictReporting === 'boolean' ? gamPredictReporting : false; + iiqAnalyticsAnalyticsAdapter.initOptions.reportingServerAddress = typeof reportEndpoint === 'string' ? reportEndpoint : ''; + iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig = typeof adUnitConfig === 'number' ? adUnitConfig : 1; } else { + logError('IIQ ANALYTICS -> there is no initialized intentIqIdSystem module') iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized = false; iiqAnalyticsAnalyticsAdapter.initOptions.partner = -1; iiqAnalyticsAnalyticsAdapter.initOptions.reportMethod = 'GET'; @@ -136,20 +160,31 @@ function initAdapterConfig() { function initReadLsIds() { try { iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = null; - iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse(readData( - `${FIRST_PARTY_KEY}${iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled ? '_p_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner : ''}`, - allowedStorage, storage - )); + iiqAnalyticsAnalyticsAdapter.initOptions.fpid = JSON.parse( + readData( + `${FIRST_PARTY_KEY}${ + iiqAnalyticsAnalyticsAdapter.initOptions.siloEnabled + ? '_p_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + : '' + }`, + allowedStorage, + storage + ) + ); if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid) { iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup = iiqAnalyticsAnalyticsAdapter.initOptions.fpid.group; } - const partnerData = readData(FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, allowedStorage, storage); + const partnerData = readData( + FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, + allowedStorage, + storage + ); const clientsHints = readData(CLIENT_HINTS_KEY, allowedStorage, storage) || ''; if (partnerData) { iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized = true; const pData = JSON.parse(partnerData); - iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause + iiqAnalyticsAnalyticsAdapter.initOptions.terminationCause = pData.terminationCause; iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs = pData.data; iiqAnalyticsAnalyticsAdapter.initOptions.eidl = pData.eidl || -1; iiqAnalyticsAnalyticsAdapter.initOptions.clientType = pData.clientType || null; @@ -158,34 +193,79 @@ function initReadLsIds() { iiqAnalyticsAnalyticsAdapter.initOptions.rrtt = pData.rrtt || null; } - iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints + iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints = clientsHints; } catch (e) { - logError(e) + logError(e); } } -function bidWon(args, isReportExternal) { +function shouldSubscribeOnGAM() { + const iiqConfig = getIntentIqConfig(); + if (!iiqConfig?.params?.gamObjectReference || !isPlainObject(iiqConfig.params.gamObjectReference)) return false; + const partnerDataFromLS = readData( + FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, + allowedStorage, + storage + ); + + if (partnerDataFromLS) { + const partnerData = JSON.parse(partnerDataFromLS); + return partnerData.gpr || (!('gpr' in partnerData) && iiqAnalyticsAnalyticsAdapter.initOptions.gamPredictReporting); + } + return false; +} + +function shouldSendReport(isReportExternal) { + return ( + (isReportExternal && + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled && + !shouldSubscribeOnGAM()) || + (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) + ); +} + +export function restoreReportList() { + reportList = {}; +} + +function checkAndInitConfig() { if (!iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized) { initAdapterConfig(); } +} - if (isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || iiqAnalyticsAnalyticsAdapter.initOptions.partner === -1) return; +function bidWon(args, isReportExternal) { + checkAndInitConfig(); + if ( + isNaN(iiqAnalyticsAnalyticsAdapter.initOptions.partner) || + iiqAnalyticsAnalyticsAdapter.initOptions.partner === -1 + ) { + return; + } const currentBrowserLowerCase = detectBrowser(); if (iiqAnalyticsAnalyticsAdapter.initOptions.browserBlackList?.includes(currentBrowserLowerCase)) { logError('IIQ ANALYTICS -> Browser is in blacklist!'); return; } - if (iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized && !iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { + if ( + iiqAnalyticsAnalyticsAdapter.initOptions.lsValueInitialized && + !iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized + ) { initReadLsIds(); } - if ((isReportExternal && iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled) || (!isReportExternal && !iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled)) { - const { url, method, payload } = constructFullUrl(preparePayload(args, true)); + if (shouldSendReport(isReportExternal)) { + const preparedPayload = preparePayload(args, true); + if (!preparedPayload) return false; + const { url, method, payload } = constructFullUrl(preparedPayload); if (method === 'POST') { - ajax(url, undefined, payload, {method, contentType: 'application/x-www-form-urlencoded'}); + ajax(url, undefined, payload, { + method, + contentType: 'application/x-www-form-urlencoded' + }); } else { - ajax(url, undefined, null, {method}); + ajax(url, undefined, null, { method }); } logInfo('IIQ ANALYTICS -> BID WON'); return true; @@ -214,17 +294,18 @@ function defineGlobalVariableName() { const iiqConfig = getIntentIqConfig(); const partnerId = iiqConfig?.params?.partner || 0; + globalName = `intentIqAnalyticsAdapter_${partnerId}`; - window[`intentIqAnalyticsAdapter_${partnerId}`] = { reportExternalWin }; + window[globalName] = { reportExternalWin }; } function getRandom(start, end) { - return Math.floor((Math.random() * (end - start + 1)) + start); + return Math.floor(Math.random() * (end - start + 1) + start); } export function preparePayload(data) { const result = getDefaultDataObject(); - readData(FIRST_PARTY_KEY + '_' + iiqAnalyticsAnalyticsAdapter.initOptions.partner, allowedStorage, storage); + result[PARAMS_NAMES.partnerId] = iiqAnalyticsAnalyticsAdapter.initOptions.partner; result[PARAMS_NAMES.prebidVersion] = prebidVersion; result[PARAMS_NAMES.referrer] = getReferrer(); @@ -238,11 +319,27 @@ export function preparePayload(data) { result[PARAMS_NAMES.isInTestGroup] = iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup === 'A'; result[PARAMS_NAMES.agentId] = REPORTER_ID; - if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pcid) result[PARAMS_NAMES.firstPartyId] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid); - if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pid) result[PARAMS_NAMES.profile] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pid) - + if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pcid) { + result[PARAMS_NAMES.firstPartyId] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid); + } + if (iiqAnalyticsAnalyticsAdapter.initOptions.fpid?.pid) { + result[PARAMS_NAMES.profile] = encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pid); + } prepareData(data, result); + if (!reportList[result.placementId] || !reportList[result.placementId][result.prebidAuctionId]) { + reportList[result.placementId] = reportList[result.placementId] + ? { ...reportList[result.placementId], [result.prebidAuctionId]: 1 } + : { [result.prebidAuctionId]: 1 }; + cleanReportsID = setTimeout(() => { + if (cleanReportsID) clearTimeout(cleanReportsID); + restoreReportList(); + }, 1500); // clear object in 1.5 second after defining reporting list + } else { + logError('Duplication detected, report will be not sent'); + return; + } + fillEidsData(result); return result; @@ -250,12 +347,13 @@ export function preparePayload(data) { function fillEidsData(result) { if (iiqAnalyticsAnalyticsAdapter.initOptions.lsIdsInitialized) { - result[PARAMS_NAMES.hadEidsInLocalStorage] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; + result[PARAMS_NAMES.hadEidsInLocalStorage] = + iiqAnalyticsAnalyticsAdapter.initOptions.eidl && iiqAnalyticsAnalyticsAdapter.initOptions.eidl > 0; result[PARAMS_NAMES.auctionEidsLength] = iiqAnalyticsAnalyticsAdapter.initOptions.eidl || -1; } } -function prepareData (data, result) { +function prepareData(data, result) { const adTypeValue = data.adType || data.mediaType; if (data.bidderCode) { @@ -276,16 +374,14 @@ function prepareData (data, result) { if (data.status) { result.status = data.status; } - if (data.auctionId) { - result.prebidAuctionId = data.auctionId; - } + + result.prebidAuctionId = data.auctionId || data.prebidAuctionId; + if (adTypeValue) { result[PARAMS_NAMES.adType] = adTypeValue; } - const iiqConfig = getIntentIqConfig(); - const adUnitConfig = iiqConfig.params?.adUnitConfig; - switch (adUnitConfig) { + switch (iiqAnalyticsAnalyticsAdapter.initOptions.adUnitConfig) { case 1: // adUnitCode or placementId result.placementId = data.adUnitCode || extractPlacementId(data) || ''; @@ -307,7 +403,7 @@ function prepareData (data, result) { result.placementId = data.adUnitCode || extractPlacementId(data) || ''; } - result.biddingPlatformId = 1; + result.biddingPlatformId = data.biddingPlatformId || 1; result.partnerAuctionId = 'BW'; } @@ -327,18 +423,18 @@ function extractPlacementId(data) { function getDefaultDataObject() { return { - 'inbbl': false, - 'pbjsver': prebidVersion, - 'partnerAuctionId': 'BW', - 'reportSource': 'pbjs', - 'abGroup': 'U', - 'jsversion': VERSION, - 'partnerId': -1, - 'biddingPlatformId': 1, - 'idls': false, - 'ast': -1, - 'aeidln': -1 - } + inbbl: false, + pbjsver: prebidVersion, + partnerAuctionId: 'BW', + reportSource: 'pbjs', + abGroup: 'U', + jsversion: VERSION, + partnerId: -1, + biddingPlatformId: 1, + idls: false, + ast: -1, + aeidln: -1 + }; } function constructFullUrl(data) { @@ -351,27 +447,38 @@ function constructFullUrl(data) { const cmpData = getCmpData(); const baseUrl = reportingServerAddress(...getDataForDefineURL()); - let url = baseUrl + '?pid=' + iiqAnalyticsAnalyticsAdapter.initOptions.partner + - '&mct=1' + - ((iiqAnalyticsAnalyticsAdapter.initOptions?.fpid) - ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) : '') + - '&agid=' + REPORTER_ID + - '&jsver=' + VERSION + - '&source=' + PREBID + - '&uh=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints) + - (cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '') + - (cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '') + - (cmpData.gdprString - ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' - : '&gdpr=0'); - url = appendSPData(url, iiqAnalyticsAnalyticsAdapter.initOptions.fpid) + let url = + baseUrl + + '?pid=' + + iiqAnalyticsAnalyticsAdapter.initOptions.partner + + '&mct=1' + + (iiqAnalyticsAnalyticsAdapter.initOptions?.fpid + ? '&iiqid=' + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.fpid.pcid) + : '') + + '&agid=' + + REPORTER_ID + + '&jsver=' + + VERSION + + '&source=' + + PREBID + + '&uh=' + + encodeURIComponent(iiqAnalyticsAnalyticsAdapter.initOptions.clientsHints) + + (cmpData.uspString ? '&us_privacy=' + encodeURIComponent(cmpData.uspString) : '') + + (cmpData.gppString ? '&gpp=' + encodeURIComponent(cmpData.gppString) : '') + + (cmpData.gdprString ? '&gdpr_consent=' + encodeURIComponent(cmpData.gdprString) + '&gdpr=1' : '&gdpr=0'); + url = appendSPData(url, iiqAnalyticsAnalyticsAdapter.initOptions.fpid); url = appendVrrefAndFui(url, iiqAnalyticsAnalyticsAdapter.initOptions.domainName); if (reportMethod === 'POST') { return { url, method: 'POST', payload: JSON.stringify(report) }; } url += '&payload=' + encodeURIComponent(JSON.stringify(report)); - url = handleAdditionalParams(currentBrowserLowerCase, url, 2, iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams); + url = handleAdditionalParams( + currentBrowserLowerCase, + url, + 2, + iiqAnalyticsAnalyticsAdapter.initOptions.additionalParams + ); return { url, method: 'GET' }; } @@ -379,6 +486,7 @@ iiqAnalyticsAnalyticsAdapter.originEnableAnalytics = iiqAnalyticsAnalyticsAdapte iiqAnalyticsAnalyticsAdapter.enableAnalytics = function (myConfig) { iiqAnalyticsAnalyticsAdapter.originEnableAnalytics(myConfig); // call the base class function + initAdapterConfig(myConfig) }; adapterManager.registerAnalyticsAdapter({ adapter: iiqAnalyticsAnalyticsAdapter, diff --git a/modules/intentIqAnalyticsAdapter.md b/modules/intentIqAnalyticsAdapter.md index 2f601658a3d..9389cb7d8ee 100644 --- a/modules/intentIqAnalyticsAdapter.md +++ b/modules/intentIqAnalyticsAdapter.md @@ -4,45 +4,61 @@ Module Name: iiqAnalytics Module Type: Analytics Adapter Maintainer: julian@intentiq.com -# Description +### Description By using this Intent IQ adapter, you will be able to obtain comprehensive analytics and metrics regarding the performance of the Intent IQ Unified ID module. This includes how the module impacts your revenue, CPMs, and fill rates related to bidders and domains. -## Intent IQ Universal ID Registration +#### Intent IQ Universal ID Registration No registration for this module is required. -## Intent IQ Universal IDConfiguration +#### Intent IQ Universal ID Configuration -IMPORTANT: only effective when Intent IQ Universal ID module is installed and configured. [(How-To)](https://docs.prebid.org/dev-docs/modules/userid-submodules/intentiq.html) +**IMPORTANT**: only effective when Intent IQ Universal ID module be installed and configured. [(How-To)](https://docs.prebid.org/dev-docs/modules/userid-submodules/intentiq.html) + +### Analytics Options + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| options.manualWinReportEnabled | Optional | Boolean | This variable determines whether the bidWon event is triggered automatically. If set to false, the event will occur automatically, and manual reporting with reportExternalWin will be disabled. If set to true, the event will not occur automatically, allowing manual reporting through reportExternalWin. The default value is false. | `false` | +| options.reportMethod | Optional | String | Defines the HTTP method used to send the analytics report. If set to `"POST"`, the report payload will be sent in the body of the request. If set to `"GET"` (default), the payload will be included as a query parameter in the request URL. | `"GET"` | +| options.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | +| options.adUnitConfig | Optional | Number | Determines how the `placementId` parameter is extracted in the report (default is 1). Possible values: 1 – adUnitCode first, 2 – placementId first, 3 – only adUnitCode, 4 – only placementId. | `1` | +| options.gamPredictReporting | Optional | Boolean | This variable controls whether the GAM prediction logic is enabled or disabled. The main purpose of this logic is to extract information from a rendered GAM slot when no Prebid bidWon event is available. In that case, we take the highest CPM from the current auction and add 0.01 to that value. | `false` | #### Example Configuration ```js pbjs.enableAnalytics({ - provider: 'iiqAnalytics' + provider: 'iiqAnalytics', + options: { + manualWinReportEnabled: false, + reportMethod: "GET", + adUnitConfig: 1, + gamPredictReporting: false + } }); ``` - ### Manual Report Trigger with reportExternalWin The reportExternalWin function allows for manual reporting, meaning that reports will not be sent automatically but only when triggered manually. To enable this manual reporting functionality, you must set the manualWinReportEnabled parameter in Intent IQ Unified ID module configuration is true. Once enabled, reports can be manually triggered using the reportExternalWin function. - ### Calling the reportExternalWin Function To call the reportExternalWin function, you need to pass the partner_id parameter as shown in the example below: ```js -window.intentIqAnalyticsAdapter_[partner_id].reportExternalWin() +window.intentIqAnalyticsAdapter_[partner_id].reportExternalWin(reportData) ``` + Example use with Partner ID = 123455 ```js -window.intentIqAnalyticsAdapter_123455.reportExternalWin() +window.intentIqAnalyticsAdapter_123455.reportExternalWin(reportData) ``` ### Function Parameters @@ -60,11 +76,12 @@ currency: 'USD', // Currency for the CPM value. originalCpm: 1.5, // Original CPM value. originalCurrency: 'USD', // Original currency. status: 'rendered', // Auction status, e.g., 'rendered'. -placementId: 'div-1', // ID of the ad placement. +placementId: 'div-1' // ID of the ad placement. adType: 'banner' // Specifies the type of ad served } ``` +{: .table .table-bordered .table-striped } | Field | Data Type | Description | Example | Mandatory | |--------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|-----------| | biddingPlatformId | Integer | Specify the platform in which this ad impression was rendered – 1 – Prebid, 2 – Amazon, 3 – Google, 4 – Open RTB (including your local Prebid server) | 1 | Yes | @@ -79,7 +96,6 @@ adType: 'banner' // Specifies the type of ad served | placementId | String | Unique identifier of the ad unit on the webpage that showed this ad | div-1 | No | | adType | String | Specifies the type of ad served. Possible values: “banner“, “video“, “native“, “audio“. | banner | No | - To report the auction win, call the function as follows: ```js diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index df26b3ce2b3..53755afa050 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -5,7 +5,7 @@ * @requires module:modules/userId */ -import {logError, isPlainObject, isStr, isNumber, getWinDimensions} from '../src/utils.js'; +import {logError, isPlainObject, isStr, isNumber} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' import {detectBrowser} from '../libraries/intentIqUtils/detectBrowserUtils.js'; @@ -21,7 +21,7 @@ import { CLIENT_HINTS_KEY, EMPTY, GVLID, - VERSION, INVALID_ID, SCREEN_PARAMS, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, + VERSION, INVALID_ID, SYNC_REFRESH_MILL, META_DATA_CONSTANT, PREBID, HOURS_24, CH_KEYS } from '../libraries/intentIqConstants/intentIqConstants.js'; import {SYNC_KEY} from '../libraries/intentIqUtils/getSyncKey.js'; @@ -73,36 +73,11 @@ function generateGUID() { return guid; } -function collectDeviceInfo() { - const windowDimensions = getWinDimensions(); - return { - windowInnerHeight: windowDimensions.innerHeight, - windowInnerWidth: windowDimensions.innerWidth, - devicePixelRatio: windowDimensions.devicePixelRatio, - windowScreenHeight: windowDimensions.screen.height, - windowScreenWidth: windowDimensions.screen.width, - language: navigator.language - } -} - function addUniquenessToUrl(url) { url += '&tsrnd=' + Math.floor(Math.random() * 1000) + '_' + new Date().getTime(); return url; } -function appendDeviceInfoToUrl(url, deviceInfo) { - const screenParamsString = Object.entries(SCREEN_PARAMS) - .map(([index, param]) => { - const value = (deviceInfo)[param]; - return `${index}:${value}`; - }) - .join(','); - - url += `&cz=${encodeURIComponent(screenParamsString)}`; - url += `&dw=${deviceInfo.windowScreenWidth}&dh=${deviceInfo.windowScreenHeight}&dpr=${deviceInfo.devicePixelRatio}&lan=${deviceInfo.language}`; - return url; -} - function appendFirstPartyData (url, firstPartyData, partnerData) { url += firstPartyData.pid ? '&pid=' + encodeURIComponent(firstPartyData.pid) : ''; url += firstPartyData.pcid ? '&iiqidtype=2&iiqpcid=' + encodeURIComponent(firstPartyData.pcid) : ''; @@ -169,7 +144,6 @@ function addMetaData(url, data) { } export function createPixelUrl(firstPartyData, clientHints, configParams, partnerData, cmpData) { - const deviceInfo = collectDeviceInfo(); const browser = detectBrowser(); let url = iiqPixelServerAddress(configParams, cmpData.gdprString); @@ -179,7 +153,6 @@ export function createPixelUrl(firstPartyData, clientHints, configParams, partne url = appendPartnersFirstParty(url, configParams); url = addUniquenessToUrl(url); url += partnerData?.clientType ? '&idtype=' + partnerData.clientType : ''; - if (deviceInfo) url = appendDeviceInfoToUrl(url, deviceInfo); url += VERSION ? '&jsver=' + VERSION : ''; if (clientHints) url += '&uh=' + encodeURIComponent(clientHints); url = appendVrrefAndFui(url, configParams.domainName); @@ -219,7 +192,8 @@ function sendSyncRequest(allowedStorage, url, partner, firstPartyData, newUser) * @param {string} gamParameterName - The name of the GAM targeting parameter where the group value will be stored. * @param {string} userGroup - The A/B testing group assigned to the user (e.g., 'A', 'B', or a custom value). */ -export function setGamReporting(gamObjectReference, gamParameterName, userGroup) { +export function setGamReporting(gamObjectReference, gamParameterName, userGroup, isBlacklisted = false) { + if (isBlacklisted) return; if (isPlainObject(gamObjectReference) && gamObjectReference.cmd) { gamObjectReference.cmd.push(() => { gamObjectReference @@ -350,7 +324,12 @@ export const intentIqIdSubmodule = { const gdprDetected = cmpData.gdprString; firstPartyData = tryParse(readData(FIRST_PARTY_KEY_FINAL, allowedStorage)); const isGroupB = firstPartyData?.group === WITHOUT_IIQ; - setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group); + const currentBrowserLowerCase = detectBrowser(); + const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; + const isBlacklisted = browserBlackList?.includes(currentBrowserLowerCase); + let newUser = false; + + setGamReporting(gamObjectReference, gamParameterName, firstPartyData?.group, isBlacklisted); if (groupChanged) groupChanged(firstPartyData?.group || NOT_YET_DEFINED); @@ -359,10 +338,6 @@ export const intentIqIdSubmodule = { }, configParams.timeoutInMillis || 500 ); - const currentBrowserLowerCase = detectBrowser(); - const browserBlackList = typeof configParams.browserBlackList === 'string' ? configParams.browserBlackList.toLowerCase() : ''; - let newUser = false; - if (!firstPartyData?.pcid) { const firstPartyId = generateGUID(); firstPartyData = { @@ -478,7 +453,7 @@ export const intentIqIdSubmodule = { } // Check if current browser is in blacklist - if (browserBlackList?.includes(currentBrowserLowerCase)) { + if (isBlacklisted) { logError('User ID - intentIqId submodule: browser is in blacklist! Data will be not provided.'); if (configParams.callback) configParams.callback(''); @@ -617,6 +592,12 @@ export const intentIqIdSubmodule = { // server provided data firstPartyData.spd = respJson.spd; } + if ('gpr' in respJson) { + // GAM prediction reporting + partnerData.gpr = respJson.gpr; + } else { + delete partnerData.gpr // remove prediction flag in case server doesn't provide it + } if (rrttStrtTime && rrttStrtTime > 0) { partnerData.rrtt = Date.now() - rrttStrtTime; diff --git a/modules/intentIqIdSystem.md b/modules/intentIqIdSystem.md index 42acf8a0600..bf561649566 100644 --- a/modules/intentIqIdSystem.md +++ b/modules/intentIqIdSystem.md @@ -15,7 +15,7 @@ By leveraging the Intent IQ identity graph, our module helps publishers, SSPs, a ## Registration Navigate to [our portal](https://www.intentiq.com/) and contact our team for partner ID. -check our [documentation](https://pbmodule.documents.intentiq.com/) to get more information about our solution and how utilze it's full potential +check our [documentation](https://pbmodule.documents.intentiq.com/) to get more information about our solution and how to utilze it's full potential ## Integration @@ -43,24 +43,20 @@ Please find below list of parameters that could be used in configuring Intent IQ | params.callback | Optional | Function | This is a callback which is triggered with data | `(data) => console.log({ data })` | | params.timeoutInMillis | Optional | Number | This is the timeout in milliseconds, which defines the maximum duration before the callback is triggered. The default value is 500. | `450` | | params.browserBlackList | Optional | String | This is the name of a browser that can be added to a blacklist. | `"chrome"` | -| params.manualWinReportEnabled | Optional | Boolean | This variable determines whether the bidWon event is triggered automatically. If set to false, the event will occur automatically, and manual reporting with reportExternalWin will be disabled. If set to true, the event will not occur automatically, allowing manual reporting through reportExternalWin. The default value is false. | `true` | | params.domainName | Optional | String | Specifies the domain of the page in which the IntentIQ object is currently running and serving the impression. This domain will be used later in the revenue reporting breakdown by domain. For example, cnn.com. It identifies the primary source of requests to the IntentIQ servers, even within nested web pages. | `"currentDomain.com"` | | params.gamObjectReference | Optional | Object | This is a reference to the Google Ad Manager (GAM) object, which will be used to set targeting. If this parameter is not provided, the group reporting will not be configured. | `googletag` | | params.gamParameterName | Optional | String | The name of the targeting parameter that will be used to pass the group. If not specified, the default value is `intent_iq_group`. | `"intent_iq_group"` | -| params.adUnitConfig | Optional | Number | Determines how the `placementId` parameter is extracted in the report (default is 1). Possible values: 1 – adUnitCode first, 2 – placementId first, 3 – only adUnitCode, 4 – only placementId | `1` | | params.sourceMetaData | Optional | String | This metadata can be provided by the partner and will be included in the requests URL as a query parameter | `"123.123.123.123"` | | params.sourceMetaDataExternal | Optional | Number | This metadata can be provided by the partner and will be included in the requests URL as a query parameter | `123456` | | params.iiqServerAddress | Optional | String | The base URL for the IntentIQ API server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | | params.iiqPixelServerAddress | Optional | String | The base URL for the IntentIQ pixel synchronization server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | -| params.reportingServerAddress | Optional | String | The base URL for the IntentIQ reporting server. If parameter is provided in `configParams`, it will be used. | `"https://domain.com"` | -| params.reportMethod | Optional | String | Defines the HTTP method used to send the analytics report. If set to `"POST"`, the report payload will be sent in the body of the request. If set to `"GET"` (default), the payload will be included as a query parameter in the request URL. |`"GET"` | | params.siloEnabled | Optional | Boolean | Determines if first-party data is stored in a siloed storage key. When set to `true`, first-party data is stored under a modified key that appends `_p_` plus the partner value rather than using the default storage key. The default value is `false`. | `true` | | params.groupChanged | Optional | Function | A callback that is triggered every time the user’s A/B group is set or updated. |`(group) => console.log('Group changed:', group)` | | params.chTimeout | Optional | Number | Maximum time (in milliseconds) to wait for Client Hints from the browser before sending request. Default value is `10ms` | `30` | -| params.additionalParameters | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | -| params.additionalParameters [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | -| params.additionalParameters [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | -| params.additionalParameters [0].destination | Required | Array | Array of numbers either `1` or `0`. Controls where this parameter is sent `[sendWithSync, sendWithVr, winreport]`. | `[1, 0, 0]` | +| params.additionalParams | Optional | Array | This parameter allows sending additional custom key-value parameters with specific destination logic (sync, VR, winreport). Each custom parameter is defined as an object in the array. | `[ { parameterName: “abc”, parameterValue: 123, destination: [1,1,0] } ]` | +| params.additionalParams [0].parameterName | Required | String | Name of the custom parameter. This will be sent as a query parameter. | `"abc"` | +| params.additionalParams [0].parameterValue | Required | String / Number | Value to assign to the parameter. | `123` | +| params.additionalParams [0].destination | Required | Array | Array of numbers either `1` or `0`. Controls where this parameter is sent `[sendWithSync, sendWithVr, winreport]`. | `[1, 0, 0]` | ### Configuration example @@ -75,15 +71,13 @@ pbjs.setConfig({ browserBlackList: "chrome", callback: (data) => {...}, // your logic here groupChanged: (group) => console.log('Group is', group), - manualWinReportEnabled: true, // Optional parameter domainName: "currentDomain.com", gamObjectReference: googletag, // Optional parameter gamParameterName: "intent_iq_group", // Optional parameter - adUnitConfig: 1, // Extracting placementId strategy (adUnitCode or placementId order of priorities) sourceMetaData: "123.123.123.123", // Optional parameter sourceMetaDataExternal: 123456, // Optional parameter - reportMethod: "GET", // Optional parameter - additionalParameters: [ // Optional parameter + chTimeout: 10, // Optional parameter + additionalParams: [ // Optional parameter { parameterName: "abc", parameterValue: 123, diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 9c970b6d509..3a4df75762b 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {deepAccess, isArray, logWarn, isFn, isPlainObject, logError, logInfo, getWinDimensions} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; diff --git a/modules/jwplayerBidAdapter.js b/modules/jwplayerBidAdapter.js index 3151e15c5a4..9ee7312bb84 100644 --- a/modules/jwplayerBidAdapter.js +++ b/modules/jwplayerBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { isArray, isFn, deepAccess, deepSetValue, logError, logWarn } from '../src/utils.js'; diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 1146ea77692..1909112d36d 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -172,6 +172,7 @@ function buildRequests(validBidRequests, bidderRequest) { const page = {} if (validPageId) { + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one page.id = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID); } if (validPageTimestamp) { diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index fcb9637f166..2b0c55b2fb9 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -1,6 +1,5 @@ import { deepAccess, - generateUUID, getWindowSelf, isArray, isStr, @@ -15,8 +14,6 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.j const additionalData = new WeakMap(); -export const pageViewId = generateUUID(); - export function setAdditionalData(obj, key, value) { const prevValue = additionalData.get(obj) || {}; additionalData.set(obj, { ...prevValue, [key]: value }); @@ -185,7 +182,7 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { kobler: { tcf_purpose_2_given: purpose2Given, tcf_purpose_3_given: purpose3Given, - page_view_id: pageViewId + page_view_id: bidderRequest.pageViewId } } }; diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 2cb24c3d360..40728c54245 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -47,7 +47,8 @@ export const spec = { { code: 'anzuExchange' }, { code: 'adnimation' }, { code: 'rtbdemand' }, - { code: 'altstar' } + { code: 'altstar' }, + { code: 'vaayaMedia' } ], supportedMediaTypes: [BANNER, VIDEO], diff --git a/modules/marsmediaBidAdapter.js b/modules/marsmediaBidAdapter.js index 5d279c49582..3a7039d7899 100644 --- a/modules/marsmediaBidAdapter.js +++ b/modules/marsmediaBidAdapter.js @@ -1,5 +1,5 @@ 'use strict'; -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, parseSizesInput, isArray, getWindowTop, deepSetValue, triggerPixel, getWindowSelf, isPlainObject } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index f9c866a09c9..3d7598e1ca9 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, isStr, replaceAuctionPrice, triggerPixel, parseGPTSingleSizeArrayToRtbSize, isEmpty } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 2c85448c14f..a4eeb591d90 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { cleanObj, deepAccess, diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index ef38369c83e..734e2120b7b 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -397,6 +397,9 @@ function addS2sInfo(auctionObj, bidderRequests) { bidObjs.forEach((bidObj) => { bidObj.serverLatencyMillis = bidderRequest.serverResponseTimeMs; + bidObj.pbsExt = Object.fromEntries( + Object.entries(bidderRequest.pbsExt || {}).filter(([key]) => key !== 'debug') + ); const serverError = deepAccess(bidderRequest, `serverErrors.0`); if (serverError && bidObj.status !== BID_SUCCESS) { bidObj.status = PBS_ERROR_STATUS_START + serverError.code; @@ -619,7 +622,10 @@ function auctionInitHandler(eventType, auction) { }); // addUidData - const userIds = deepAccess(auction.bidderRequests, '0.bids.0.userId'); + let userIds; + if (typeof getGlobal().getUserIds === 'function') { + userIds = getGlobal().getUserIds(); + } if (isPlainObject(userIds)) { const enabledUids = mnetGlobals.configuration.enabledUids || []; auctionObj.availableUids = Object.keys(userIds).sort(); diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 68019f9c06b..8044768bdad 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { _each, deepAccess, diff --git a/modules/msftBidAdapter.js b/modules/msftBidAdapter.js new file mode 100644 index 00000000000..6155b91e778 --- /dev/null +++ b/modules/msftBidAdapter.js @@ -0,0 +1,576 @@ +import { + ortbConverter +} from "../libraries/ortbConverter/converter.js"; +import { + registerBidder +} from "../src/adapters/bidderFactory.js"; +import { + BANNER, + NATIVE, + VIDEO +} from "../src/mediaTypes.js"; +import { + Renderer +} from "../src/Renderer.js"; +import { + getStorageManager +} from "../src/storageManager.js"; +import { + hasPurpose1Consent +} from "../src/utils/gdpr.js"; +import { + deepAccess, + deepSetValue, + getParameterByName, + isArray, + isArrayOfNums, + isNumber, + isStr, + logError, + logMessage, + logWarn, + mergeDeep +} from "../src/utils.js"; + +const BIDDER_CODE = "msft"; +const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; +const DEBUG_QUERY_PARAM_MAP = { + 'apn_debug_enabled': 'enabled', + 'apn_debug_dongle': 'dongle', + 'apn_debug_member_id': 'member_id', + 'apn_debug_timeout': 'debug_timeout' +}; +const ENDPOINT_URL_NORMAL = "https://ib.adnxs.com/openrtb2/prebidjs"; +const ENDPOINT_URL_SIMPLE = "https://ib.adnxs-simple.com/openrtb2/prebidjs"; +const GVLID = 32; +const RESPONSE_MEDIA_TYPE_MAP = { + 0: BANNER, + 1: VIDEO, + 3: NATIVE +}; +const SOURCE = "pbjs"; + +const storage = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +/** + * STUFF FOR REQUEST SIDE + * + * list of old appnexus bid params -> how to set them now for msft adapter -> where are they in the openRTB request + * params.placement_id -> params.placement_id -> ext.appnexus.placement_id DONE + * params.member -> params.member -> query string as `member_id` DONE + * params.inv_code -> params.inv_code -> imp.tagid DONE + * params.publisher_id -> ortb.publisher.id -> publisher.id DONE + * params.frameworks -> params.banner_frameworks -> banner.api (array of ints) DONE + * params.user -> ortb.user -> user DONE + * params.allow_smaller_sizes -> params.allow_smaller_sizes -> imp.ext.appnexus.allow_smaller_sizes DONE + * params.use_pmt_rule -> params.use_pmt_rule -> ext.appnexus.use_pmt_rule (boolean) DONE + * params.keywords -> params.keywords (for tag level keywords) -> imp.ext.appnexus.keywords (comma delimited string) DONE + * params.video -> mediaTypes.video -> imp.video DONE + * params.video.frameworks -> mediatypes.video.api -> imp.video.api (array of ints) DONE + * params.app -> ortb.app -> app DONE + * params.reserve -> bidfloor module -> imp.bidfloor DONE + * params.position -> mediaTypes.banner.pos -> imp.banner.pos DONE + * params.traffic_source_code -> params.traffic_source_code -> imp.ext.appnexus.traffic_source_code DONE + * params.supply_type -> ortb.site/app -> site/app DONE + * params.pub_click -> params.pubclick -> imp.ext.appnexus.pubclick DONE + * params.ext_inv_code -> params.ext_inv_code -> imp.ext.appnexus.ext_inv_code DONE + * params.external_imp_id -> params.ext_imp_id -> imp.id (overrides default imp.id) DONE + * + * list of ut.tags[] fields that weren't tied to bid params -> where they were read before -> where they go in the openRTB request + * uuid -> set in adapter -> imp.id DONE + * primary_size -> imp.banner.w and imp.banner.h (if not already set from mediaTypes.banner.sizes) DONE + * sizes -> mediaTypes.banner.sizes -> imp.banner.format DONE + * ad_types -> mediaTypes.banner/video/native -> imp.banner/video/native DONE + * gpid -> ortb2Imp.ext.gpid (from ortb) -> imp.ext.gpid (from ortb) DONE + * tid -> ortb.source.tid (from ortb) -> source.tid DONE? + * hb_source -> set in adapter -> ext.appnexus.hb_source DONE + * native -> mediaTypes.native (ORTB version) -> imp.native DONE + * + * list of ut fields that weren't tied to bid params -> where they were read before -> where they go in the openRTB request + * schain -> set in adapter from bidRequest.schain -> source.ext.schain DONE + * iab_support -> set in adapter from mediaTypes.video.api and bid params.frameworks -> source.ext.omidpn and source.ext.omidpv DONE + * device -> was part of bid.params.app (read now from ortb.device) -> device DONE + * keywords -> getConfig('appnexusAuctionKeywords') (read now from ortb.site/user) -> site/user DONE + * gdpr_consent -> set in adapter from bidderRequest.gdprConsent -> regs.ext.gdpr and user.ext.consent DONE + * privacy -> set in adapter from bidderRequest.uspConsent -> regs.ext.privacy DONE + * eids -> set in adapter from bidRequest.userId -> user.ext.eids DONE + * dsa -> set in adapter from ortb.regs.ext.dsa -> regs.ext.dsa DONE + * coppa -> getConfig('coppa') -> regs.coppa DONE + * require_asset_url -> mediaTypes.video.context === 'instream' -> imp.ext.appnexus.require_asset_url DONE + */ + +/** + * STUFF FOR RESPONSE SIDE + * + * new bid response fields ib is adding + * old field from UT -> new field in ortb bid response -> where it goes in the bidResponse object + * advertiser_id -> imp.ext.appnexus.advertiser_id -> bidResponse.advertiserId DONE + * renderer_config -> imp.ext.appnexus.renderer_config -> bidResponse.rendererConfig DONE + * renderer_id -> imp.ext.appnexus.renderer_id -> bidResponse.rendererId DONE + * asset_url -> imp.ext.appnexus.asset_url -> bidResponse.assetUrl DONE + * + */ + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, + ttl: 300, + }, + imp(buildImp, bidRequest, context) { + const extANData = {}; + const bidderParams = bidRequest.params; + const imp = buildImp(bidRequest, context); + // banner.topframe, banner.format, banner.pos are handled in processors/banner.js + // video.mimes, video.protocols, video.w, video.h, video.startdelay are handled in processors/video.js + // native request is handled in processors/native.js + if (imp.banner && !imp.banner.w && !imp.banner.h) { + const primarySizeObj = deepAccess(imp, 'banner.format.0'); + if (primarySizeObj && isNumber(primarySizeObj.w) && isNumber(primarySizeObj.h)) { + imp.banner.w = primarySizeObj.w; + imp.banner.h = primarySizeObj.h; + } + } + + if (imp?.banner && !imp.banner.api) { + const bannerFrameworks = bidderParams.banner_frameworks; + if (isArrayOfNums(bannerFrameworks)) { + imp.banner.api = bannerFrameworks; + } + } + + if (FEATURES.VIDEO && imp?.video) { + if (deepAccess(bidRequest, 'mediaTypes.video.context') === 'instream') { + extANData.require_asset_url = true; + } + + if (imp.video.plcmt) { + imp.video.placement = imp.video.plcmt; + delete imp.video.plcmt; + } + } + + if (bidderParams) { + if (bidderParams.placement_id) { + extANData.placement_id = bidderParams.placement_id; + } else if (bidderParams.inv_code) { + deepSetValue(imp, 'tagid', bidderParams.inv_code); + } + + const optionalParamsTypeMap = { + allow_smaller_sizes: 'boolean', + use_pmt_rule: 'boolean', + keywords: 'string', + traffic_source_code: 'string', + pubclick: 'string', + ext_inv_code: 'string', + ext_imp_id: 'string' + }; + Object.entries(optionalParamsTypeMap).forEach(([paramName, paramType]) => { + if (checkOptionalParams(bidRequest, paramName, paramType)) { + if (paramName === 'ext_imp_id') { + imp.id = bidderParams.ext_imp_id; + return; + } + extANData[paramName] = bidderParams[paramName]; + } + }); + } + + // for force creative we expect the following format: + // page.html?ast_override_div=divId:creativeId,divId2:creativeId2 + const overrides = getParameterByName('ast_override_div'); + if (isNotEmptyString(overrides)) { + const adUnitOverride = decodeURIComponent(overrides).split(',').find((pair) => pair.startsWith(`${bidRequest.adUnitCode}:`)); + if (adUnitOverride) { + const forceCreativeId = adUnitOverride.split(':')[1]; + if (forceCreativeId) { + extANData.force_creative_id = parseInt(forceCreativeId, 10); + } + } + } + + if (Object.keys(extANData).length > 0) { + deepSetValue(imp, 'ext.appnexus', extANData); + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + if (request?.user?.ext?.eids?.length > 0) { + request.user.ext.eids.forEach(eid => { + if (eid.source === 'adserver.org') { + eid.rti_partner = 'TDID'; + } else if (eid.source === 'uidapi.com') { + eid.rti_partner = 'UID2'; + } + }); + } + + const extANData = { + prebid: true, + hb_source: 1, + sdk: { + version: '$prebid.version$', + source: SOURCE + } + }; + + if (bidderRequest?.refererInfo) { + const refererinfo = { + rd_ref: encodeURIComponent(bidderRequest.refererInfo.topmostLocation), + rd_top: bidderRequest.refererInfo.reachedTop, + rd_ifs: bidderRequest.refererInfo.numIframes, + rd_stk: bidderRequest.refererInfo.stack?.map((url) => encodeURIComponent(url)).join(',') + }; + const pubPageUrl = bidderRequest.refererInfo.canonicalUrl; + if (isNotEmptyString(pubPageUrl)) { + refererinfo.rd_can = pubPageUrl; + } + extANData.referrer_detection = refererinfo; + } + + deepSetValue(request, 'ext.appnexus', extANData); + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + // first derive the mediaType from bid data + let mediaType; + const bidAdType = bid?.ext?.appnexus?.bid_ad_type; + const extANData = deepAccess(bid, 'ext.appnexus'); + + if (isNumber(bidAdType) && RESPONSE_MEDIA_TYPE_MAP.hasOwnProperty(bidAdType)) { + context.mediaType = mediaType = RESPONSE_MEDIA_TYPE_MAP[bidAdType]; + } + const bidResponse = buildBidResponse(bid, context); + + if (extANData.advertiser_id) { + bidResponse.meta = Object.assign({}, bidResponse.meta, { + advertiser_id: extANData.advertiser_id + }); + } + + // replace the placeholder token for trk.js if it's present in eventtrackers + if (FEATURES.NATIVE && mediaType === NATIVE) { + try { + const nativeAdm = bid.adm ? JSON.parse(bid.adm) : {}; + if (nativeAdm?.eventtrackers && isArray(nativeAdm.eventtrackers)) { + nativeAdm.eventtrackers.forEach(trackCfg => { + if (trackCfg.url.includes('dom_id=%native_dom_id%')) { + const prebidParams = 'pbjs_adid=' + bidResponse.adId + ';pbjs_auc=' + bidRequest.adUnitCode; + trackCfg.url = trackCfg.url.replace('dom_id=%native_dom_id%', prebidParams); + } + }); + } + } catch (e) { + logError('MSFT Native adm parse error', e); + } + } + + if (FEATURES.VIDEO && mediaType === VIDEO) { + // handle outstream bids, ie setup the renderer + if (extANData?.renderer_url && extANData?.renderer_id) { + const adUnitCode = bidRequest?.adUnitCode; + if (isNotEmptyString(adUnitCode)) { + // rendererOptions here should be treated as any publisher options for outstream ... + // ...set within the adUnit.mediaTypes.video.renderer.options or in the adUnit.renderer.options + let rendererOptions = deepAccess(bidRequest, 'mediaTypes.video.renderer.options'); + if (!rendererOptions) { + rendererOptions = deepAccess(bidRequest, 'renderer.options'); + } + + // populate imbpus config options in the bidReponse.adResponse.ad object for our outstream renderer to use later + // renderer_config should be treated as the old rtb.rendererOptions that came from the bidresponse.adResponse + if (!bidResponse.adResponse) { + bidResponse.adResponse = { + ad: { + notify_url: bid.nurl || '', + renderer_config: extANData.renderer_config || '', + }, + auction_id: extANData.auction_id, + content: bidResponse.vastXml, + tag_id: extANData.tag_id + }; + } + + bidResponse.renderer = newRenderer(adUnitCode, { + renderer_url: extANData.renderer_url, + renderer_id: extANData.renderer_id, + }, rendererOptions); + } + } else { + // handle instream bids + // if nurl and asset_url was set, we need to populate vastUrl field + if (bid.nurl && extANData?.asset_url) { + bidResponse.vastUrl = bid.nurl + '&redir=' + encodeURIComponent(extANData.asset_url); + } + // if not populated, the VAST in the adm will go to the vastXml field by the ortb converter + } + } + + return bidResponse; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: [], // TODO fill in after full transition or as seperately requested + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + + isBidRequestValid: (bid) => { + const params = bid.params; + return !!( + (typeof params.placement_id === 'number') || + (typeof params.member === 'number' && isNotEmptyString(params?.inv_code)) + ); + }, + + buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({ + bidRequests, + bidderRequest, + }); + + const omidSupport = ((bidRequests) || []).find(hasOmidSupport); + if (omidSupport) { + mergeDeep( + data, { + source: { + ext: { + omidpn: 'AppNexus', + omidpv: '$prebid.version$' + } + } + }, + data); + } + + // TODO remove later + logMessage("MSFT openRTB request", data); + + return formatRequest(data, bidderRequest); + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + + return bids; + }, + + getUserSyncs: function ( + syncOptions, + responses, + gdprConsent, + uspConsent, + gppConsent + ) { + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { + return [{ + type: "iframe", + url: "https://acdn.adnxs.com/dmp/async_usersync.html", + }, ]; + } + + if (syncOptions.pixelEnabled) { + // first attempt using static list + const imgList = ["https://px.ads.linkedin.com/setuid?partner=appNexus"]; + return imgList.map((url) => ({ + type: "image", + url, + })); + } + }, +}; + +function isNotEmptyString(value) { + return isStr(value) && value !== ''; +} + +function checkOptionalParams(bidRequest, fieldName, expectedType) { + const value = bidRequest?.params?.[fieldName]; + // allow false, but not undefined, null or empty string + if (value !== undefined && value !== null && value !== '') { + const actualType = typeof value; + if (actualType === expectedType) { + return true; + } else { + logWarn(`Removing invalid bid.param ${fieldName} for adUnitCode ${bidRequest.adUnitCode}, expected ${expectedType}`); + return false; + } + } + return false; +} + +function formatRequest(payload, bidderRequest) { + let request = []; + const options = { + withCredentials: true, + }; + + let endpointUrl = ENDPOINT_URL_NORMAL; + if (!hasPurpose1Consent(bidderRequest.gdprConsent)) { + endpointUrl = ENDPOINT_URL_SIMPLE; + } + + // handle debug info here if needed + let debugObj = {}; + const debugCookieName = 'apn_prebid_debug'; + const debugCookie = storage.getCookie(debugCookieName) || null; + + // first check cookie + if (debugCookie) { + try { + debugObj = JSON.parse(debugCookie); + } catch (e) { + logError('MSFT Debug Auction Cookie Error:\n\n' + e); + } + } else { + // then check query params + Object.keys(DEBUG_QUERY_PARAM_MAP).forEach(qparam => { + const qval = getParameterByName(qparam); + if (isStr(qval) && qval !== '') { + debugObj[DEBUG_QUERY_PARAM_MAP[qparam]] = qval; + // keep 'enabled' for old setups still using the cookie, switch to 'debug' when passing to query params + } + }); + if (Object.keys(debugObj).length > 0 && !debugObj.hasOwnProperty('enabled')) debugObj.enabled = true; + } + + if (debugObj?.enabled) { + endpointUrl += '?' + Object.keys(debugObj) + .filter(param => DEBUG_PARAMS.includes(param)) + .map(param => (param === 'enabled') ? `debug=${debugObj[param]}` : `${param}=${debugObj[param]}`) + .join('&'); + } + + // check if member is defined in the bid params + const matchingBid = ((bidderRequest?.bids) || []).find(bid => bid.params && bid.params.member && isNumber(bid.params.member)); + if (matchingBid) { + endpointUrl += (endpointUrl.indexOf('?') === -1 ? '?' : '&') + 'member_id=' + matchingBid.params.member; + } + + if (getParameterByName("apn_test").toUpperCase() === "TRUE") { + options.customHeaders = { + "X-Is-Test": 1, + }; + } + + request.push({ + method: "POST", + url: endpointUrl, + data: payload, + bidderRequest, + options, + }); + + return request; +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + adUnitCode, + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn("Prebid Error calling setRender on renderer", err); + } + + renderer.setEventHandlers({ + impression: () => logMessage("AppNexus outstream video impression event"), + loaded: () => logMessage("AppNexus outstream video loaded event"), + ended: () => { + logMessage("AppNexus outstream renderer video event"); + document.querySelector(`#${adUnitCode}`).style.display = "none"; + }, + }); + return renderer; +} + +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Appnexus renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + try { + const el = document + .getElementById(elementId) + .querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty("display", "none"); + } + } catch (e) { + // element not found! + } +} + +function hideSASIframe(elementId) { + try { + // find script tag with id 'sas_script'. This ensures it only works if you're using Smart Ad Server. + const el = document + .getElementById(elementId) + .querySelectorAll("script[id^='sas_script']"); + if (el[0]?.nextSibling?.localName === "iframe") { + el[0].nextSibling.style.setProperty("display", "none"); + } + } catch (e) { + // element not found! + } +} + +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ + id, + eventName, + }); +} + +function outstreamRender(bid, doc) { + hidedfpContainer(bid.adUnitCode); + hideSASIframe(bid.adUnitCode); + // push to render queue because ANOutstreamVideo may not be loaded yet + bid.renderer.push(() => { + const win = doc?.defaultView || window; + win.ANOutstreamVideo.renderAd({ + tagId: bid.adResponse.tag_id, + sizes: [bid.getSize().split("x")], + targetId: bid.adUnitCode, // target div id to render video + uuid: bid.requestId, + adResponse: bid.adResponse, // fix + rendererOptions: bid.renderer.getConfig(), + }, + handleOutstreamRendererEvents.bind(null, bid) + ); + }); +} + +function hasOmidSupport(bid) { + // read from mediaTypes.video.api = 7 + // read from bid.params.frameworks = 6 (for banner) + // change >> ignore bid.params.video.frameworks = 6 (prefer mediaTypes.video.api) + let hasOmid = false; + const bidderParams = bid?.params; + const videoParams = bid?.mediaTypes?.video?.api; + if (bidderParams?.frameworks && isArray(bidderParams.frameworks)) { + hasOmid = bid.params.frameworks.includes(6); + } + if (!hasOmid && isArray(videoParams) && videoParams.length > 0) { + hasOmid = videoParams.includes(7); + } + return hasOmid; +} + +registerBidder(spec); diff --git a/modules/msftBidAdapter.md b/modules/msftBidAdapter.md new file mode 100644 index 00000000000..df5be0a1fb6 --- /dev/null +++ b/modules/msftBidAdapter.md @@ -0,0 +1,272 @@ +# Overview + +``` +Module Name: Microsoft Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@microsoft.com +``` + +# Description + +The Microsoft Bid Adapter connects to Microsoft's advertising exchange for bids. This adapter supports banner, video (instream and outstream), and native ad formats using OpenRTB 2.5 standards. + +The Microsoft adapter requires setup and approval from the Microsoft Advertising team. Please reach out to your account team for more information. + +# Migration from AppNexus Bid Adapter + +## Bid Parameters + +If you are migrating from the AppNexus bid adapter, the following table shows how the bid parameters have changed: + +| AppNexus Parameter | Microsoft Parameter | Description | +|-------------------|-------------------|-------------| +| `params.placementId` | `params.placement_id` | Placement ID (**only** the underscore case is now supported) | +| `params.member` | `params.member` | Member ID (unchanged) | +| `params.inv_code` | `params.inv_code` | Inventory code (unchanged) | +| `params.publisher_id` | Use `ortb2.publisher.id` | Publisher ID (moved to ortb2 config) | +| `params.frameworks` | `params.banner_frameworks` | Banner API frameworks array | +| `params.user` | Use `ortb2.user` | User data (moved to ortb2 config) | +| `params.allow_smaller_sizes` | `params.allow_smaller_sizes` | Allow smaller ad sizes (unchanged) | +| `params.use_pmt_rule` | `params.use_pmt_rule` | Use payment rule (unchanged) | +| `params.keywords` | `params.keywords` | Tag/Imp-level keywords (use ORTB format of comma-delimited string value; eg pet=cat,food,brand=fancyfeast) | +| `params.video` | Use `mediaTypes.video` | Video parameters (moved to mediaTypes) | +| `params.video.frameworks` | Use `mediaTypes.video.api` | Video API frameworks (moved to mediaTypes) | +| `params.app` | Use `ortb2.app` | App data (moved to ortb2 config) | +| `params.reserve` | Use bidfloor module | Reserve price (use bidfloor module) | +| `params.position` | Use `mediaTypes.banner.pos` | Banner position (moved to mediaTypes) | +| `params.traffic_source_code` | `params.traffic_source_code` | Traffic source code (unchanged) | +| `params.supply_type` | Use `ortb2.site` or `ortb2.app` | Supply type (moved to ortb2 config) | +| `params.pub_click` | `params.pubclick` | Publisher click URL (dropped underscore to align to endpoint) | +| `params.ext_inv_code` | `params.ext_inv_code` | External inventory code (unchanged) | +| `params.external_imp_id` | `params.ext_imp_id` | External impression ID (shortend to ext) | + +## Migration Example + +**Before (AppNexus):** +```javascript +{ + bidder: "appnexus", + params: { + placementId: "12345", + member: "123", + publisher_id: "456", + frameworks: [1, 2], + position: "above", + reserve: 0.50, + keywords: "category=sports,team=football" + } +} +``` + +**After (Microsoft):** +```javascript +{ + bidder: "msft", + params: { + placement_id: "12345", + member: "123", + banner_frameworks: [1, 2], + keywords: "category=sports,team=football" + } +} +``` + +## Native + +If you are migrating from the AppNexus bid adapter, the setup for Native adUnits now require the use of the Prebid.js ORTB Native setup. The Microsoft Bid Adapter no longer offers support to the legacy Prebid.js Native adUnit setup. Requests using that approach will not work and need to be converted to the equivalent values in the adUnit. This change is made to better align with Prebid.js and many other Bid Adapters that support Native in an ORTB context. + +Please refer to the [Prebid.js Native Implementation Guide](https://docs.prebid.org/prebid/native-implementation.html) if you need additional information to implement the setup. + +# Test Parameters + +## Banner +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + bids: [ + { + bidder: "msft", + params: { + placement_id: "12345" + } + } + ] + } +]; +``` + +## Video +```javascript +var videoAdUnit = { + code: 'video-ad-unit', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [2, 3], + maxduration: 30, + api: [2] + } + }, + bids: [ + { + bidder: 'msft', + params: { + placement_id: "67890" + } + } + ] +}; +``` + +## Native +```javascript +var nativeAdUnit = { + code: 'native-ad-unit', + mediaTypes: { + native: { + ortb: { + ver: '1.2', + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 300 + } + }, { + id: 2, + required: 1, + title: { + len: 100, + } + }, { + id: 3, + required: 1, + data: { + type: 1 + } + }] + } + } + }, + bids: [ + { + bidder: 'msft', + params: { + placement_id: "13579" + } + } + ] +}; +``` + +## Multi-format Ad Unit +```javascript +var multiFormatAdUnit = { + code: 'multi-format-ad-unit', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + playerSize: [300, 250] + } + }, + bids: [ + { + bidder: 'msft', + params: { + member: "123", + inv_code: "test_inv_code", + allow_smaller_sizes: true, + banner_frameworks: [1, 2], + keywords: "category=news,section=sports" + } + } + ] +}; +``` + +# Supported Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `placement_id` | String | Yes* | Placement ID from Microsoft Advertising | +| `member` | String | Yes* | Member ID (required if placement_id not provided) | +| `inv_code` | String | Yes* | Inventory code (required if placement_id not provided) | +| `allow_smaller_sizes` | Boolean | No | Allow smaller ad sizes than requested | +| `use_pmt_rule` | Boolean | No | Use payment rule | +| `keywords` | String | No | Comma-delimited keywords for targeting | +| `traffic_source_code` | String | No | Traffic source identifier | +| `pubclick` | String | No | Publisher click URL | +| `ext_inv_code` | String | No | External inventory code | +| `ext_imp_id` | String | No | External impression ID | +| `banner_frameworks` | Array of Integers | No | Supported banner API frameworks | + +*Either `placement_id` OR both `member` and `inv_code` are required. + +# Configuration + +## Global Configuration +```javascript +pbjs.setConfig({ + ortb2: { + site: { + publisher: { + id: "your-publisher-id" + } + }, + user: { + keywords: "global,keywords,here" + } + } +}); +``` + +## Floor Prices +```javascript +pbjs.setConfig({ + floors: { + enforcement: { + enforceJS: true, + floorDeals: true + }, + data: { + currency: 'USD', + schema: { + delimiter: '*', + fields: ['mediaType', 'size'] + }, + values: { + 'banner*300x250': 0.50, + 'video*640x480': 1.00, + '*': 0.25 + } + } + } +}); +``` + +# User Sync + +The Microsoft adapter supports both iframe and pixel user syncing. It will attempt iframe sync first if enabled and GDPR consent is available, otherwise it will fall back to pixel sync. + +```javascript +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + pixelEnabled: true, + syncDelay: 3000 + } +}); +``` diff --git a/modules/nativeryBidAdapter.js b/modules/nativeryBidAdapter.js index caeaa891e5e..3b3dadd1d10 100644 --- a/modules/nativeryBidAdapter.js +++ b/modules/nativeryBidAdapter.js @@ -6,7 +6,9 @@ import { deepSetValue, logError, logWarn, + safeJSONEncode, } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -18,6 +20,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'nativery'; const BIDDER_ALIAS = ['nat']; const ENDPOINT = 'https://hb.nativery.com/openrtb2/auction'; +const EVENT_TRACKER_URL = 'https://hb.nativery.com/openrtb2/track-event'; +// Currently we log every event +const DEFAULT_SAMPLING_RATE = 1; +const EVENT_LOG_RANDOM_NUMBER = Math.random(); const DEFAULT_CURRENCY = 'EUR'; const TTL = 30; const MAX_IMPS_PER_REQUEST = 10; @@ -86,7 +92,7 @@ export const spec = { ); if (Array.isArray(responseErrors) && responseErrors.length > 0) { logWarn( - 'Nativery: Error in bid response ' + JSON.stringify(responseErrors) + 'Nativery: Error in bid response ' + safeJSONEncode(responseErrors) ); } const ortb = converter.fromORTB({ @@ -96,12 +102,45 @@ export const spec = { return ortb.bids ?? []; } } catch (error) { - const errMsg = error?.message ?? JSON.stringify(error); + const errMsg = error?.message ?? safeJSONEncode(error); logError('Nativery: unhandled error in bid response ' + errMsg); return []; } return []; }, + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction + */ + onBidWon: function(bid) { + if (bid == null || Object.keys(bid).length === 0) return + reportEvent('NAT_BID_WON', bid) + }, + /** + * Register bidder specific code, which will execute if the ad + * has been rendered successfully + * @param {Bid} bid Bid request object + */ + onAdRenderSucceeded: function (bid) { + if (bid == null || Object.keys(bid).length === 0) return + reportEvent('NAT_AD_RENDERED', bid) + }, + /** + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {Object} timeoutData Containing timeout specific data + */ + onTimeout: function (timeoutData) { + if (!Array.isArray(timeoutData) || timeoutData.length === 0) return + reportEvent('NAT_TIMEOUT', timeoutData) + }, + /** + * Register bidder specific code, which will execute if the bidder responded with an error + * @param {Object} errorData An object with the XMLHttpRequest error and the bid request object + */ + onBidderError: function (errorData) { + if (errorData == null || Object.keys(errorData).length === 0) return + reportEvent('NAT_BIDDER_ERROR', errorData) + } }; function formatRequest(ortbPayload) { @@ -132,4 +171,19 @@ function formatRequest(ortbPayload) { return request; } +function reportEvent(event, data, sampling = null) { + // Currently this condition is always true since DEFAULT_SAMPLING_RATE = 1, + // meaning we log every event. In the future, we may want to implement event + // sampling by lowering the sampling rate. + const samplingRate = sampling ?? DEFAULT_SAMPLING_RATE; + if (samplingRate > EVENT_LOG_RANDOM_NUMBER) { + const payload = { + prebidVersion: '$prebid.version$', + event, + data, + }; + ajax(EVENT_TRACKER_URL, undefined, safeJSONEncode(payload), { method: 'POST', withCredentials: true, keepalive: true }); + } +} + registerBidder(spec); diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 4e625163eca..3255547aa47 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -1,173 +1,318 @@ -import { deepAccess, deepSetValue, generateUUID, logError, logInfo, mergeDeep } from '../src/utils.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { ajax } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import * as events from '../src/events.js'; -import { EVENTS } from '../src/constants.js'; - -export const DATA_PROVIDER = 'neuwo.ai'; -const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 -const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' -const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' +/** + * @module neuwoRtdProvider + * @author Grzegorz Malisz + * @see {project-root-directory}/integrationExamples/gpt/neuwoRtdProvider_example.html for an example/testing page. + * @see {project-root-directory}/test/spec/modules/neuwoRtdProvider_spec.js for unit tests. + * @description + * This module is a Prebid.js Real-Time Data (RTD) provider that integrates with the Neuwo API. + * + * It fetches contextual marketing categories (IAB content and audience) for the current page from the Neuwo API. + * The retrieved data is then injected into the bid request as OpenRTB (ORTB2)`site.content.data` + * and `user.data` fragments, making it available for bidders to use in their decisioning process. + * + * @see {@link https://docs.prebid.org/dev-docs/add-rtd-submodule.html} for more information on development of Prebid.js RTD modules. + * @see {@link https://docs.prebid.org/features/firstPartyData.html} for more information on Prebid.js First Party Data. + * @see {@link https://www.neuwo.ai/} for more information on the Neuwo API. + */ + +import { ajax } from "../src/ajax.js"; +import { submodule } from "../src/hook.js"; +import { getRefererInfo } from "../src/refererDetection.js"; +import { deepSetValue, logError, logInfo, mergeDeep } from "../src/utils.js"; + +const MODULE_NAME = "NeuwoRTDModule"; +export const DATA_PROVIDER = "www.neuwo.ai"; + +// Cached API response to avoid redundant requests. +let globalCachedResponse; + +/** + * Clears the cached API response. Primarily used for testing. + * @private + */ +export function clearCache() { + globalCachedResponse = undefined; +} +// Maps the IAB Content Taxonomy version string to the corresponding segtax ID. +// Based on https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list--category-taxonomies- +const IAB_CONTENT_TAXONOMY_MAP = { + "1.0": 1, + "2.0": 2, + "2.1": 5, + "2.2": 6, + "3.0": 7, + "3.1": 9, +}; + +/** + * Validates the configuration and initialises the module. + * + * @param {Object} config The module configuration. + * @param {Object} userConsent The user consent object. + * @returns {boolean} `true` if the module is configured correctly, otherwise `false`. + */ function init(config, userConsent) { - // config.params = config.params || {} - // ignore module if publicToken is missing (module setup failure) - if (!config || !config.params || !config.params.publicToken) { - logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') + logInfo(MODULE_NAME, "init:", config, userConsent); + const params = config?.params || {}; + if (!params.neuwoApiUrl) { + logError(MODULE_NAME, "init:", "Missing Neuwo Edge API Endpoint URL"); return false; } - if (!config || !config.params || !config.params.apiUrl) { - logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') + if (!params.neuwoApiToken) { + logError(MODULE_NAME, "init:", "Missing Neuwo API Token missing"); return false; } return true; } +/** + * Fetches contextual data from the Neuwo API and enriches the bid request object with IAB categories. + * Uses cached response if available to avoid redundant API calls. + * + * @param {Object} reqBidsConfigObj The bid request configuration object. + * @param {function} callback The callback function to continue the auction. + * @param {Object} config The module configuration. + * @param {Object} config.params Configuration parameters. + * @param {string} config.params.neuwoApiUrl The Neuwo API endpoint URL. + * @param {string} config.params.neuwoApiToken The Neuwo API authentication token. + * @param {string} [config.params.websiteToAnalyseUrl] Optional URL to analyze instead of current page. + * @param {string} [config.params.iabContentTaxonomyVersion] IAB content taxonomy version (default: "3.0"). + * @param {boolean} [config.params.enableCache=true] If true, caches API responses to avoid redundant requests (default: true). + * @param {boolean} [config.params.stripAllQueryParams] If true, strips all query parameters from the URL. + * @param {string[]} [config.params.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [config.params.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [config.params.stripFragments] If true, strips URL fragments (hash). + * @param {Object} userConsent The user consent object. + */ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - const confParams = config.params || {}; - logInfo('NeuwoRTDModule', 'starting getBidRequestData') - - const wrappedArgUrl = encodeURIComponent(confParams.argUrl || getRefererInfo().page); - /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ - const joiner = confParams.apiUrl.indexOf('?') < 0 ? '?' : '&' - const url = confParams.apiUrl + joiner + [ - 'token=' + confParams.publicToken, - 'url=' + wrappedArgUrl - ].join('&') - const billingId = generateUUID(); - - const success = (responseContent) => { - logInfo('NeuwoRTDModule', 'GetAiTopics: response', responseContent) + logInfo(MODULE_NAME, "getBidRequestData:", "starting getBidRequestData", config); + + const { + websiteToAnalyseUrl, + neuwoApiUrl, + neuwoApiToken, + iabContentTaxonomyVersion, + enableCache = true, + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments, + } = config.params; + + const rawUrl = websiteToAnalyseUrl || getRefererInfo().page; + const processedUrl = cleanUrl(rawUrl, { + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments + }); + const pageUrl = encodeURIComponent(processedUrl); + // Adjusted for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') + const joiner = neuwoApiUrl.indexOf("?") < 0 ? "?" : "&"; + const neuwoApiUrlFull = + neuwoApiUrl + joiner + ["token=" + neuwoApiToken, "url=" + pageUrl].join("&"); + + const success = (response) => { + logInfo(MODULE_NAME, "getBidRequestData:", "Neuwo API raw response:", response); try { - const jsonContent = JSON.parse(responseContent); - if (jsonContent.marketing_categories) { - events.emit(EVENTS.BILLABLE_EVENT, { type: 'request', billingId, vendor: neuwoRtdModule.name }) + const responseParsed = JSON.parse(response); + + if (enableCache) { + globalCachedResponse = responseParsed; } - injectTopics(jsonContent, reqBidsConfigObj, billingId) + + injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion); } catch (ex) { - logError('NeuwoRTDModule', 'Response to JSON parse error', ex) + logError(MODULE_NAME, "getBidRequestData:", "Error while processing Neuwo API response", ex); } - callback() - } + callback(); + }; const error = (err) => { - logError('xhr error', null, err); - callback() - } + logError(MODULE_NAME, "getBidRequestData:", "AJAX error:", err); + callback(); + }; - ajax(url, {success, error}, null, { - // could assume Origin header is set, or - // customHeaders: { 'Origin': 'Origin' } - }) + if (enableCache && globalCachedResponse) { + logInfo(MODULE_NAME, "getBidRequestData:", "Using cached response:", globalCachedResponse); + injectIabCategories(globalCachedResponse, reqBidsConfigObj, iabContentTaxonomyVersion); + callback(); + } else { + logInfo(MODULE_NAME, "getBidRequestData:", "Calling Neuwo API Endpoint: ", neuwoApiUrlFull); + ajax(neuwoApiUrlFull, { success, error }, null); + } } -export function addFragment(base, path, addition) { - const container = {} - deepSetValue(container, path, addition) - mergeDeep(base, container) -} +// +// HELPER FUNCTIONS +// /** - * Concatenate a base array and an array within an object - * non-array bases will be arrays, non-arrays at object key will be discarded - * @param {Array} base base array to add to - * @param {object} source object to get an array from - * @param {string} key dot-notated path to array within object - * @returns base + source[key] if that's an array + * Cleans a URL by stripping query parameters and/or fragments based on the provided configuration. + * + * @param {string} url The URL to clean. + * @param {Object} options Cleaning options. + * @param {boolean} [options.stripAllQueryParams] If true, strips all query parameters. + * @param {string[]} [options.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [options.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [options.stripFragments] If true, strips URL fragments (hash). + * @returns {string} The cleaned URL. */ -function combineArray(base, source, key) { - if (Array.isArray(base) === false) base = [] - const addition = deepAccess(source, key, []) - if (Array.isArray(addition)) return base.concat(addition) - else return base -} +export function cleanUrl(url, options = {}) { + const { stripAllQueryParams, stripQueryParamsForDomains, stripQueryParams, stripFragments } = options; -export function injectTopics(topics, bidsConfig) { - topics = topics || {} + if (!url) { + logInfo(MODULE_NAME, "cleanUrl:", "Empty or null URL provided, returning as-is"); + return url; + } - // join arrays of IAB category details to single array - const combinedTiers = combineArray( - combineArray([], topics, RESPONSE_IAB_TIER_1), - topics, RESPONSE_IAB_TIER_2) + logInfo(MODULE_NAME, "cleanUrl:", "Input URL:", url, "Options:", options); - const segment = pickSegments(combinedTiers) - // effectively gets topics.marketing_categories.iab_tier_1, topics.marketing_categories.iab_tier_2 - // used as FPD segments content + try { + const urlObj = new URL(url); - const IABSegments = { - name: DATA_PROVIDER, - ext: { segtax: SEGTAX_IAB }, - segment - } + // Strip fragments if requested + if (stripFragments === true) { + urlObj.hash = ""; + logInfo(MODULE_NAME, "cleanUrl:", "Stripped fragment from URL"); + } + + // Option 1: Strip all query params unconditionally + if (stripAllQueryParams === true) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } - addFragment(bidsConfig.ortb2Fragments.global, 'site.content.data', [IABSegments]) + // Option 2: Strip all query params for specific domains + if (Array.isArray(stripQueryParamsForDomains) && stripQueryParamsForDomains.length > 0) { + const hostname = urlObj.hostname; + const shouldStripForDomain = stripQueryParamsForDomains.some(domain => { + // Support exact match or subdomain match + return hostname === domain || hostname.endsWith("." + domain); + }); - // upgrade category taxonomy to IAB 2.2, inject result to page categories - if (segment.length > 0) { - addFragment(bidsConfig.ortb2Fragments.global, 'site.pagecat', segment.map(s => s.id)) - } + if (shouldStripForDomain) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + } + + // Option 3: Strip specific query parameters + // Caveats: + // - "?=value" is treated as query parameter with key "" and value "value" + // - "??" is treated as query parameter with key "?" and value "" + if (Array.isArray(stripQueryParams) && stripQueryParams.length > 0) { + const queryParams = urlObj.searchParams; + logInfo(MODULE_NAME, "cleanUrl:", `Query parameters to strip: ${stripQueryParams}`); + stripQueryParams.forEach(param => { + queryParams.delete(param); + }); + urlObj.search = queryParams.toString(); + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } - logInfo('NeuwoRTDModule', 'injectTopics: post-injection bidsConfig', bidsConfig) + const finalUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", finalUrl); + return finalUrl; + } catch (e) { + logError(MODULE_NAME, "cleanUrl:", "Error cleaning URL:", e); + return url; + } } -const D_IAB_ID = { // Content Taxonomy version 2.0 final release November 2017 [sic] (Taxonomy ID Mapping, IAB versions 2.0 - 2.2) - 'IAB19-1': '603', 'IAB6-1': '193', 'IAB5-2': '133', 'IAB20-1': '665', 'IAB20-2': '656', 'IAB23-2': '454', 'IAB3-2': '102', 'IAB20-3': '672', 'IAB8-5': '211', - 'IAB8-18': '211', 'IAB7-4': '288', 'IAB7-5': '233', 'IAB17-12': '484', 'IAB19-3': '608', 'IAB21-1': '442', 'IAB9-2': '248', 'IAB15-1': '456', 'IAB9-17': '265', 'IAB20-4': '658', - 'IAB2-3': '30', 'IAB2-1': '32', 'IAB17-1': '518', 'IAB2-2': '34', 'IAB2': '1', 'IAB8-2': '215', 'IAB17-2': '545', 'IAB17-26': '547', 'IAB9-3': '249', 'IAB18-1': '553', 'IAB20-5': '674', - 'IAB15-2': '465', 'IAB3-3': '119', 'IAB16-2': '423', 'IAB9-4': '259', 'IAB9-5': '270', 'IAB18-2': '574', 'IAB17-4': '549', 'IAB7-33': '312', 'IAB1-1': '42', 'IAB17-5': '485', 'IAB23-3': '458', - 'IAB20-6': '675', 'IAB3': '53', 'IAB20-7': '676', 'IAB19-5': '633', 'IAB20-9': '677', 'IAB9-6': '250', 'IAB17-6': '499', 'IAB2-4': '25', 'IAB9-7': '271', 'IAB4-11': '125', 'IAB4-1': '126', - 'IAB4': '123', 'IAB16-3': '424', 'IAB2-5': '18', 'IAB17-7': '486', 'IAB15-3': '466', 'IAB23-5': '459', 'IAB9-9': '260', 'IAB2-22': '19', 'IAB17-8': '500', 'IAB9-10': '261', 'IAB5-5': '137', - 'IAB9-11': '262', 'IAB2-21': '3', 'IAB19-2': '610', 'IAB19-8': '600', 'IAB19-9': '601', 'IAB3-5': '121', 'IAB9-15': '264', 'IAB2-6': '8', 'IAB2-7': '9', 'IAB22-2': '474', 'IAB17-9': '491', - 'IAB2-8': '10', 'IAB20-12': '678', 'IAB17-3': '492', 'IAB19-12': '611', 'IAB14-1': '188', 'IAB6-3': '194', 'IAB7-17': '316', 'IAB19-13': '612', 'IAB8-8': '217', 'IAB9-1': '205', 'IAB19-22': '613', - 'IAB8-9': '218', 'IAB14-2': '189', 'IAB16-4': '425', 'IAB9-12': '251', 'IAB5': '132', 'IAB6-9': '190', 'IAB19-15': '623', 'IAB17-17': '496', 'IAB20-14': '659', 'IAB6': '186', 'IAB20-26': '666', - 'IAB17-10': '510', 'IAB13-4': '396', 'IAB1-3': '201', 'IAB16-1': '426', 'IAB17-11': '511', 'IAB17-13': '511', 'IAB17-32': '511', 'IAB7-1': '225', 'IAB8': '210', 'IAB8-10': '219', 'IAB9-13': '266', - 'IAB10-4': '275', 'IAB9-14': '273', 'IAB15-8': '469', 'IAB15-4': '470', 'IAB17-15': '512', 'IAB3-7': '77', 'IAB19-16': '614', 'IAB3-8': '78', 'IAB2-10': '22', 'IAB2-12': '22', 'IAB2-11': '11', - 'IAB8-12': '221', 'IAB7-35': '223', 'IAB7-38': '223', 'IAB7-24': '296', 'IAB13-5': '411', 'IAB7-25': '234', 'IAB23-6': '460', 'IAB9': '239', 'IAB7-26': '235', 'IAB10': '274', 'IAB10-1': '278', - 'IAB10-2': '279', 'IAB19-17': '634', 'IAB10-5': '280', 'IAB5-10': '145', 'IAB5-11': '146', 'IAB20-17': '667', 'IAB17-16': '497', 'IAB20-18': '668', 'IAB3-9': '55', 'IAB1-4': '440', 'IAB17-18': '514', - 'IAB17-27': '515', 'IAB10-3': '282', 'IAB19-25': '618', 'IAB17-19': '516', 'IAB13-6': '398', 'IAB10-7': '283', 'IAB12-1': '382', 'IAB19-24': '624', 'IAB6-4': '195', 'IAB23-7': '461', 'IAB9-19': '252', - 'IAB4-4': '128', 'IAB4-5': '127', 'IAB23-8': '462', 'IAB10-8': '284', 'IAB5-8': '147', 'IAB16-5': '427', 'IAB11-2': '383', 'IAB12-3': '384', 'IAB3-10': '57', 'IAB2-13': '23', 'IAB9-20': '241', - 'IAB3-1': '58', 'IAB3-11': '58', 'IAB14-4': '191', 'IAB17-20': '520', 'IAB7-31': '228', 'IAB7-37': '301', 'IAB3-12': '107', 'IAB2-14': '13', 'IAB17-25': '519', 'IAB2-15': '27', 'IAB1-5': '324', - 'IAB1-6': '338', 'IAB9-16': '243', 'IAB13-8': '412', 'IAB12-2': '385', 'IAB9-21': '253', 'IAB8-6': '222', 'IAB7-32': '229', 'IAB2-16': '14', 'IAB17-23': '521', 'IAB13-9': '413', 'IAB17-24': '501', - 'IAB9-22': '254', 'IAB15-5': '244', 'IAB6-2': '196', 'IAB6-5': '197', 'IAB6-6': '198', 'IAB2-17': '24', 'IAB13-2': '405', 'IAB13': '391', 'IAB13-7': '410', 'IAB13-12': '415', 'IAB16': '422', - 'IAB9-23': '255', 'IAB7-36': '236', 'IAB15-6': '471', 'IAB2-18': '15', 'IAB11-4': '386', 'IAB1-2': '432', 'IAB5-9': '139', 'IAB6-7': '305', 'IAB5-12': '149', 'IAB5-13': '134', 'IAB19-4': '631', - 'IAB19-19': '631', 'IAB19-20': '631', 'IAB19-32': '631', 'IAB9-24': '245', 'IAB21': '441', 'IAB21-3': '451', 'IAB23': '453', 'IAB10-9': '276', 'IAB4-9': '130', 'IAB16-6': '429', 'IAB4-6': '129', - 'IAB13-10': '416', 'IAB2-19': '28', 'IAB17-28': '525', 'IAB9-25': '272', 'IAB17-29': '527', 'IAB17-30': '227', 'IAB17-31': '530', 'IAB22-1': '481', 'IAB15': '464', 'IAB9-26': '246', 'IAB9-27': '256', - 'IAB9-28': '267', 'IAB17-33': '502', 'IAB19-35': '627', 'IAB2-20': '4', 'IAB7-39': '307', 'IAB19-30': '605', 'IAB22': '473', 'IAB17-34': '503', 'IAB17-35': '531', 'IAB7-19': '309', 'IAB7-40': '310', - 'IAB19-6': '635', 'IAB7-41': '237', 'IAB17-36': '504', 'IAB17-44': '533', 'IAB20-23': '662', 'IAB15-7': '472', 'IAB20-24': '671', 'IAB5-14': '136', 'IAB6-8': '199', 'IAB17': '483', 'IAB9-29': '263', - 'IAB2-23': '5', 'IAB13-11': '414', 'IAB4-3': '395', 'IAB18': '552', 'IAB7-42': '311', 'IAB17-37': '505', 'IAB17-38': '537', 'IAB17-39': '538', 'IAB19-26': '636', 'IAB19': '596', 'IAB1-7': '640', - 'IAB17-40': '539', 'IAB7-43': '293', 'IAB20': '653', 'IAB8-16': '212', 'IAB8-17': '213', 'IAB16-7': '430', 'IAB9-30': '680', 'IAB17-41': '541', 'IAB17-42': '542', 'IAB17-43': '506', 'IAB15-10': '390', - 'IAB19-23': '607', 'IAB19-34': '629', 'IAB14-7': '165', 'IAB7-44': '231', 'IAB7-45': '238', 'IAB9-31': '257', 'IAB5-1': '135', 'IAB7-2': '301', 'IAB18-6': '580', 'IAB7-3': '297', 'IAB23-1': '453', - 'IAB8-1': '214', 'IAB7-6': '312', 'IAB7-7': '300', 'IAB7-8': '301', 'IAB13-1': '410', 'IAB7-9': '301', 'IAB15-9': '465', 'IAB7-10': '313', 'IAB3-4': '602', 'IAB20-8': '660', 'IAB8-3': '214', - 'IAB20-10': '660', 'IAB7-11': '314', 'IAB20-11': '660', 'IAB23-4': '459', 'IAB9-8': '270', 'IAB8-4': '214', 'IAB7-12': '306', 'IAB7-13': '313', 'IAB7-14': '287', 'IAB18-5': '575', 'IAB7-15': '315', - 'IAB8-7': '214', 'IAB19-11': '616', 'IAB7-16': '289', 'IAB7-18': '301', 'IAB7-20': '290', 'IAB20-13': '659', 'IAB7-21': '313', 'IAB18-3': '579', 'IAB13-3': '52', 'IAB20-15': '659', 'IAB8-11': '214', - 'IAB7-22': '318', 'IAB20-16': '659', 'IAB7-23': '313', 'IAB7': '223', 'IAB10-6': '634', 'IAB7-27': '318', 'IAB11-1': '388', 'IAB7-29': '318', 'IAB7-30': '304', 'IAB19-18': '619', 'IAB8-13': '214', - 'IAB20-19': '659', 'IAB20-20': '657', 'IAB8-14': '214', 'IAB18-4': '565', 'IAB23-9': '459', 'IAB11': '379', 'IAB8-15': '214', 'IAB20-21': '662', 'IAB17-21': '492', 'IAB17-22': '518', 'IAB12': '379', - 'IAB23-10': '453', 'IAB7-34': '301', 'IAB4-8': '395', 'IAB26-3': '608', 'IAB20-25': '151', 'IAB20-27': '659' +/** + * Injects data into the OpenRTB 2.x global fragments of the bid request object. + * + * @param {Object} reqBidsConfigObj The main bid request configuration object. + * @param {string} path The dot-notation path where the data should be injected (e.g., 'site.content.data'). + * @param {*} data The data to inject at the specified path. + */ +export function injectOrtbData(reqBidsConfigObj, path, data) { + const container = {}; + deepSetValue(container, path, data); + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, container); } -export function convertSegment(segment) { - if (!segment) return {} - return { - id: D_IAB_ID[segment.id || segment.ID] - } +/** + * Builds an IAB category data object for use in OpenRTB. + * + * @param {Object} marketingCategories Marketing Categories returned by Neuwo API. + * @param {string[]} tiers The tier keys to extract from marketingCategories. + * @param {number} segtax The IAB taxonomy version Id. + * @returns {Object} The constructed data object. + */ +export function buildIabData(marketingCategories, tiers, segtax) { + const data = { + name: DATA_PROVIDER, + segment: [], + ext: { segtax }, + }; + + tiers.forEach((tier) => { + const tierData = marketingCategories?.[tier]; + if (Array.isArray(tierData)) { + tierData.forEach((item) => { + const ID = item?.ID; + const label = item?.label; + + if (ID && label) { + data.segment.push({ id: ID, name: label }); + } + }); + } + }); + + return data; } /** - * map array of objects to segments - * @param {Array<{ID: string}>} normalizable - * @returns array of IAB "segments" + * Processes the Neuwo API response to build and inject IAB content and audience categories + * into the bid request object. + * + * @param {Object} responseParsed The parsed JSON response from the Neuwo API. + * @param {Object} reqBidsConfigObj The bid request configuration object to be modified. + * @param {string} iabContentTaxonomyVersion The version of the IAB content taxonomy to use for segtax mapping. */ -export function pickSegments(normalizable) { - if (Array.isArray(normalizable) === false) return [] - return normalizable.map(convertSegment) - .filter(t => t.id) +function injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion) { + const marketingCategories = responseParsed.marketing_categories; + + if (!marketingCategories) { + logError(MODULE_NAME, "injectIabCategories:", "No Marketing Categories in Neuwo API response."); + return + } + + // Process content categories + const contentTiers = ["iab_tier_1", "iab_tier_2", "iab_tier_3"]; + const contentData = buildIabData( + marketingCategories, + contentTiers, + IAB_CONTENT_TAXONOMY_MAP[iabContentTaxonomyVersion] || IAB_CONTENT_TAXONOMY_MAP["3.0"] + ); + + // Process audience categories + const audienceTiers = ["iab_audience_tier_3", "iab_audience_tier_4", "iab_audience_tier_5"]; + const audienceData = buildIabData(marketingCategories, audienceTiers, 4); + + logInfo(MODULE_NAME, "injectIabCategories:", "contentData structure:", contentData); + logInfo(MODULE_NAME, "injectIabCategories:", "audienceData structure:", audienceData); + + injectOrtbData(reqBidsConfigObj, "site.content.data", [contentData]); + injectOrtbData(reqBidsConfigObj, "user.data", [audienceData]); + + logInfo(MODULE_NAME, "injectIabCategories:", "post-injection bidsConfig", reqBidsConfigObj); } export const neuwoRtdModule = { - name: 'NeuwoRTDModule', + name: MODULE_NAME, init, - getBidRequestData -} + getBidRequestData, +}; -submodule('realTimeData', neuwoRtdModule) +submodule("realTimeData", neuwoRtdModule); diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index fb52734d451..804130be1e6 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -1,51 +1,204 @@ # Overview -Module Name: Neuwo Rtd Provider -Module Type: Rtd Provider -Maintainer: neuwo.ai + Module Name: Neuwo Rtd Provider + Module Type: Rtd Provider + Maintainer: Grzegorz Malisz (grzegorz.malisz@neuwo.ai) -# Description +## Description -The Neuwo AI RTD module is an advanced AI solution for real-time data processing in the field of contextual targeting and advertising. With its cutting-edge algorithms, it allows advertisers to target their audiences with the highest level of precision based on context, while also delivering a seamless user experience. +The Neuwo RTD provider fetches real-time contextual data from the Neuwo API. When installed, the module retrieves IAB content and audience categories relevant to the current page's content. -The module provides advertiser with valuable insights and real-time contextual bidding capabilities. Whether you're a seasoned advertising professional or just starting out, Neuwo AI RTD module is the ultimate tool for contextual targeting and advertising. +This data is then added to the bid request by populating the OpenRTB 2.x objects `ortb2.site.content.data` (for IAB Content Taxonomy) and `ortb2.user.data` (for IAB Audience Taxonomy). This enrichment allows bidders to leverage Neuwo's contextual analysis for more precise targeting and decision-making. -The benefit of Neuwo AI RTD module is that it provides an alternative solution for advertisers to target their audiences and deliver relevant advertisements, as the widespread use of cookies for tracking and targeting is becoming increasingly limited. +Here is an example scheme of the data injected into the `ortb2` object by our module: -The RTD module uses cutting-edge algorithms to process real-time data, allowing advertisers to target their audiences based on contextual information, such as segments, IAB Tiers and brand safety. The RTD module is designed to be flexible and scalable, making it an ideal solution for advertisers looking to stay ahead of the curve in the post-cookie era. +```javascript +ortb2: { + site: { + content: { + // IAB Content Taxonomy data is injected here + data: [{ + name: "www.neuwo.ai", + segment: [{ + id: "274", + name: "Home & Garden", + }, + { + id: "42", + name: "Books and Literature", + }, + { + id: "210", + name: "Food & Drink", + }, + ], + ext: { + segtax: 7, + }, + }, ], + }, + }, + user: { + // IAB Audience Taxonomy data is injected here + data: [{ + name: "www.neuwo.ai", + segment: [{ + id: "49", + name: "Demographic | Gender | Female |", + }, + { + id: "161", + name: "Demographic | Marital Status | Married |", + }, + { + id: "6", + name: "Demographic | Age Range | 30-34 |", + }, + ], + ext: { + segtax: 4, + }, + }, ], + }, +} +``` + +To get started, you can generate your API token at [https://neuwo.ai/generatetoken/](https://neuwo.ai/generatetoken/), send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). + +## Configuration -Generate your token at: [https://neuwo.ai/generatetoken/] +> **Important:** You must add the domain (origin) where Prebid.js is running to the list of allowed origins in Neuwo Edge API configuration. If you have problems, send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). -# Configuration +This module is configured as part of the `realTimeData.dataProviders` object. ```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + enableCache: true, // Default: true. Caches API responses to avoid redundant requests + }, + }, + ], + }, +}); +``` -const neuwoDataProvider = { - name: 'NeuwoRTDModule', - params: { - publicToken: '', - apiUrl: '' - } -} -pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) +**Parameters** + +| Name | Type | Required | Default | Description | +| :---------------------------------- | :------- | :------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | Yes | | The name of the module, which is `NeuwoRTDModule`. | +| `params` | Object | Yes | | Container for module-specific parameters. | +| `params.neuwoApiUrl` | String | Yes | | The endpoint URL for the Neuwo Edge API. | +| `params.neuwoApiToken` | String | Yes | | Your unique API token provided by Neuwo. | +| `params.iabContentTaxonomyVersion` | String | No | `'3.0'` | Specifies the version of the IAB Content Taxonomy to be used. Supported values: `'2.2'`, `'3.0'`. | +| `params.enableCache` | Boolean | No | `true` | If `true`, caches API responses to avoid redundant requests for the same page during the session. Set to `false` to disable caching and make a fresh API call on every bid request. | +| `params.stripAllQueryParams` | Boolean | No | `false` | If `true`, strips all query parameters from the URL before analysis. Takes precedence over other stripping options. | +| `params.stripQueryParamsForDomains` | String[] | No | `[]` | List of domains for which to strip **all** query parameters. When a domain matches, all query params are removed for that domain and all its subdomains (e.g., `'example.com'` strips params for both `'example.com'` and `'sub.example.com'`). This option takes precedence over `stripQueryParams` for matching domains. | +| `params.stripQueryParams` | String[] | No | `[]` | List of specific query parameter names to strip from the URL (e.g., `['utm_source', 'fbclid']`). Other parameters are preserved. Only applies when the domain does not match `stripQueryParamsForDomains`. | +| `params.stripFragments` | Boolean | No | `false` | If `true`, strips URL fragments (hash, e.g., `#section`) from the URL before analysis. | + +### API Response Caching +By default, the module caches API responses during the page session to optimise performance and reduce redundant API calls. This behaviour can be disabled by setting `enableCache: false` if needed for dynamic content scenarios. + +### URL Cleaning Options + +The module provides optional URL cleaning capabilities to strip query parameters and/or fragments from the analysed URL before sending it to the Neuwo API. This can be useful for privacy, caching, or analytics purposes. + +**Example with URL cleaning:** + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + + // Option 1: Strip all query parameters from the URL + stripAllQueryParams: true, + + // Option 2: Strip all query parameters only for specific domains + // stripQueryParamsForDomains: ['example.com', 'another-domain.com'], + + // Option 3: Strip specific query parameters by name + // stripQueryParams: ['utm_source', 'utm_campaign', 'fbclid'], + + // Optional: Strip URL fragments (hash) + stripFragments: true, + }, + }, + ], + }, +}); ``` -# Testing +## Local Development -`gulp test --modules=rtdModule,neuwoRtdProvider` +Install the exact versions of packages specified in the lockfile: -## Add development tools if necessary +```bash +npm ci +``` -- Install node for npm -- run in prebid.js source folder: -`npm ci` -`npm i -g gulp-cli` +> **Linux** Linux might require exporting the following environment variable before running the commands below: +> `export CHROME_BIN=/usr/bin/chromium` -## Serve +You can run a local development server with the Neuwo module and a test bid adapter using the following command: -`gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter` +```bash +npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter +``` -- in your browser, navigate to: +For a faster build without tests: -`http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html` +```bash +npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter +``` + +After starting the server, you can access the example page at: +[http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html) + +### Add development tools if necessary + +If you don't have gulp-cli installed globally, run the following command in your Prebid.js source folder: + +```bash +npm i -g gulp-cli +``` + +## Linting + +To lint the module: + +```bash +npx eslint 'modules/neuwoRtdProvider.js' --cache --cache-strategy content +``` + +## Testing + +To run the module-specific tests: + +```bash +npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/euwoRtdProvider_spec.js +``` + +Skip building, if the project has already been built: + +```bash +npx gulp test-only-nobuild --file=test/spec/modules/neuwoRtdProvider_spec.js +``` diff --git a/modules/nexverseBidAdapter.js b/modules/nexverseBidAdapter.js index 6617ea2890e..25d52000a82 100644 --- a/modules/nexverseBidAdapter.js +++ b/modules/nexverseBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; import { isArray, generateUUID, getWinDimensions, isNumber } from '../src/utils.js'; diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js deleted file mode 100644 index c89238b9242..00000000000 --- a/modules/nexx360BidAdapter.js +++ /dev/null @@ -1,192 +0,0 @@ -import { deepSetValue, generateUUID, logError, logInfo } from '../src/utils.js'; -import {getStorageManager} from '../src/storageManager.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {getGlobal} from '../src/prebidGlobal.js'; -import {ortbConverter} from '../libraries/ortbConverter/converter.js' - -import { createResponse, enrichImp, enrichRequest, getAmxId, getUserSyncs } from '../libraries/nexx360Utils/index.js'; -import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; - -/** - * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest - * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid - * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse - * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions - * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync - * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests - */ - -const BIDDER_CODE = 'nexx360'; -const REQUEST_URL = 'https://fast.nexx360.io/booster'; -const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '6.3'; -const GVLID = 965; -const NEXXID_KEY = 'nexx360_storage'; - -const ALIASES = [ - { code: 'revenuemaker' }, - { code: 'first-id', gvlid: 1178 }, - { code: 'adwebone' }, - { code: 'league-m', gvlid: 965 }, - { code: 'prjads' }, - { code: 'pubtech' }, - { code: '1accord', gvlid: 965 }, - { code: 'easybid', gvlid: 1068 }, - { code: 'prismassp', gvlid: 965 }, - { code: 'spm', gvlid: 965 }, - { code: 'bidstailamedia', gvlid: 965 }, - { code: 'scoremedia', gvlid: 965 }, - { code: 'movingup', gvlid: 1416 }, - { code: 'glomexbidder', gvlid: 967 }, - { code: 'revnew', gvlid: 1468 }, - { code: 'pubxai', gvlid: 1485 }, -]; - -export const STORAGE = getStorageManager({ - bidderCode: BIDDER_CODE, -}); - -/** - * Get the NexxId - * @param - * @return {object | false } false if localstorageNotEnabled - */ - -export function getNexx360LocalStorage() { - if (!STORAGE.localStorageIsEnabled()) { - logInfo(`localstorage not enabled for Nexx360`); - return false; - } - const output = STORAGE.getDataFromLocalStorage(NEXXID_KEY); - if (output === null) { - const nexx360Storage = { nexx360Id: generateUUID() }; - STORAGE.setDataInLocalStorage(NEXXID_KEY, JSON.stringify(nexx360Storage)); - return nexx360Storage; - } - try { - return JSON.parse(output) - } catch (e) { - return false; - } -} - -const converter = ortbConverter({ - context: { - netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false - ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) - }, - imp(buildImp, bidRequest, context) { - let imp = buildImp(bidRequest, context); - imp = enrichImp(imp, bidRequest); - const divId = bidRequest.params.divId || bidRequest.adUnitCode; - const slotEl = document.getElementById(divId); - if (slotEl) { - const { width, height } = getBoundingClientRect(slotEl); - deepSetValue(imp, 'ext.dimensions.slotW', width); - deepSetValue(imp, 'ext.dimensions.slotH', height); - deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); - deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); - } - if (bidRequest.params.tagId) deepSetValue(imp, 'ext.nexx360.tagId', bidRequest.params.tagId); - if (bidRequest.params.placement) deepSetValue(imp, 'ext.nexx360.placement', bidRequest.params.placement); - if (bidRequest.params.videoTagId) deepSetValue(imp, 'ext.nexx360.videoTagId', bidRequest.params.videoTagId); - if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); - if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); - if (bidRequest.params.allBids) deepSetValue(imp, 'ext.nexx360.allBids', bidRequest.params.allBids); - return imp; - }, - request(buildRequest, imps, bidderRequest, context) { - let request = buildRequest(imps, bidderRequest, context); - const amxId = getAmxId(STORAGE, BIDDER_CODE); - request = enrichRequest(request, amxId, bidderRequest, PAGE_VIEW_ID, BIDDER_VERSION); - return request; - }, -}); - -/** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ -function isBidRequestValid(bid) { - if (bid.params.adUnitName && (typeof bid.params.adUnitName !== 'string' || bid.params.adUnitName === '')) { - logError('bid.params.adUnitName needs to be a string'); - return false; - } - if (bid.params.adUnitPath && (typeof bid.params.adUnitPath !== 'string' || bid.params.adUnitPath === '')) { - logError('bid.params.adUnitPath needs to be a string'); - return false; - } - if (bid.params.divId && (typeof bid.params.divId !== 'string' || bid.params.divId === '')) { - logError('bid.params.divId needs to be a string'); - return false; - } - if (bid.params.allBids && typeof bid.params.allBids !== 'boolean') { - logError('bid.params.allBids needs to be a boolean'); - return false; - } - if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId && !bid.params.placement) { - logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId or bid.params.placement must be defined'); - return false; - } - return true; -}; - -/** - * Make a server request from the list of BidRequests. - * - * @return ServerRequest Info describing the request to the server. - */ - -function buildRequests(bidRequests, bidderRequest) { - const data = converter.toORTB({bidRequests, bidderRequest}) - return { - method: 'POST', - url: REQUEST_URL, - data, - } -} - -/** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - -function interpretResponse(serverResponse) { - const respBody = serverResponse.body; - if (!respBody || !Array.isArray(respBody.seatbid)) { - return []; - } - - const { bidderSettings } = getGlobal(); - const allowAlternateBidderCodes = bidderSettings && bidderSettings.standard ? bidderSettings.standard.allowAlternateBidderCodes : false; - - const responses = []; - for (let i = 0; i < respBody.seatbid.length; i++) { - const seatbid = respBody.seatbid[i]; - for (let j = 0; j < seatbid.bid.length; j++) { - const bid = seatbid.bid[j]; - const response = createResponse(bid, respBody); - if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; - responses.push(response); - } - } - return responses; -} - -export const spec = { - code: BIDDER_CODE, - gvlid: GVLID, - aliases: ALIASES, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs, -}; - -registerBidder(spec); diff --git a/modules/nexx360BidAdapter.ts b/modules/nexx360BidAdapter.ts new file mode 100644 index 00000000000..b5c27bd288b --- /dev/null +++ b/modules/nexx360BidAdapter.ts @@ -0,0 +1,168 @@ +import { deepSetValue, generateUUID, logError } from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {AdapterRequest, BidderSpec, registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +import { interpretResponse, enrichImp, enrichRequest, getAmxId, getLocalStorageFunctionGenerator, getUserSyncs } from '../libraries/nexx360Utils/index.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; +import { BidRequest, ClientBidderRequest } from '../src/adapterManager.js'; +import { ORTBImp, ORTBRequest } from '../src/prebid.public.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'nexx360'; +const REQUEST_URL = 'https://fast.nexx360.io/booster'; +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '7.1'; +const GVLID = 965; +const NEXXID_KEY = 'nexx360_storage'; + +const DEFAULT_GZIP_ENABLED = false; + +type RequireAtLeastOne = + Omit & { + [K in Keys]-?: Required> & + Partial>> + }[Keys]; + +type Nexx360BidParams = RequireAtLeastOne<{ + tagId?: string; + placement?: string; + videoTagId?: string; + nativeTagId?: string; + adUnitPath?: string; + adUnitName?: string; + divId?: string; + allBids?: boolean; + customId?: string; + bidders?: Record; +}, "tagId" | "placement">; + +declare module '../src/adUnits' { + interface BidderParams { + [BIDDER_CODE]: Nexx360BidParams; + } +} + +const ALIASES = [ + { code: 'revenuemaker' }, + { code: 'first-id', gvlid: 1178 }, + { code: 'adwebone' }, + { code: 'league-m', gvlid: 965 }, + { code: 'prjads' }, + { code: 'pubtech' }, + { code: '1accord', gvlid: 965 }, + { code: 'easybid', gvlid: 1068 }, + { code: 'prismassp', gvlid: 965 }, + { code: 'spm', gvlid: 965 }, + { code: 'bidstailamedia', gvlid: 965 }, + { code: 'scoremedia', gvlid: 965 }, + { code: 'movingup', gvlid: 1416 }, + { code: 'glomexbidder', gvlid: 967 }, + { code: 'revnew', gvlid: 1468 }, + { code: 'pubxai', gvlid: 1485 }, + { code: 'ybidder', gvlid: 1253 }, +]; + +export const STORAGE = getStorageManager({ + bidderCode: BIDDER_CODE, +}); + +export const getNexx360LocalStorage = getLocalStorageFunctionGenerator<{ nexx360Id: string }>( + STORAGE, + BIDDER_CODE, + NEXXID_KEY, + 'nexx360Id' +); + +export const getGzipSetting = (): boolean => { + const getBidderConfig = config.getBidderConfig(); + if (getBidderConfig.nexx360?.gzipEnabled === 'true') { + return getBidderConfig.nexx360?.gzipEnabled === 'true'; + } + return DEFAULT_GZIP_ENABLED; +} + +const converter = ortbConverter({ + context: { + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest: BidRequest, context) { + let imp:ORTBImp = buildImp(bidRequest, context); + imp = enrichImp(imp, bidRequest); + const divId = bidRequest.params.divId || bidRequest.adUnitCode; + const slotEl:HTMLElement | null = typeof divId === 'string' ? document.getElementById(divId) : null; + if (slotEl) { + const { width, height } = getBoundingClientRect(slotEl); + deepSetValue(imp, 'ext.dimensions.slotW', width); + deepSetValue(imp, 'ext.dimensions.slotH', height); + deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); + deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); + } + deepSetValue(imp, 'ext.nexx360', bidRequest.params); + deepSetValue(imp, 'ext.nexx360.divId', divId); + if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); + if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + let request:ORTBRequest = buildRequest(imps, bidderRequest, context); + const amxId = getAmxId(STORAGE, BIDDER_CODE); + request = enrichRequest(request, amxId, PAGE_VIEW_ID, BIDDER_VERSION); + return request; + }, +}); + +const isBidRequestValid = (bid:BidRequest): boolean => { + if (bid.params.adUnitName && (typeof bid.params.adUnitName !== 'string' || bid.params.adUnitName === '')) { + logError('bid.params.adUnitName needs to be a string'); + return false; + } + if (bid.params.adUnitPath && (typeof bid.params.adUnitPath !== 'string' || bid.params.adUnitPath === '')) { + logError('bid.params.adUnitPath needs to be a string'); + return false; + } + if (bid.params.divId && (typeof bid.params.divId !== 'string' || bid.params.divId === '')) { + logError('bid.params.divId needs to be a string'); + return false; + } + if (bid.params.allBids && typeof bid.params.allBids !== 'boolean') { + logError('bid.params.allBids needs to be a boolean'); + return false; + } + if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId && !bid.params.placement) { + logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId or bid.params.placement must be defined'); + return false; + } + return true; +}; + +const buildRequests = ( + bidRequests: BidRequest[], + bidderRequest: ClientBidderRequest, +): AdapterRequest => { + const data:ORTBRequest = converter.toORTB({bidRequests, bidderRequest}) + const adapterRequest:AdapterRequest = { + method: 'POST', + url: REQUEST_URL, + data, + options: { + endpointCompression: getGzipSetting() + }, + } + return adapterRequest; +} + +export const spec:BidderSpec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ALIASES, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/nodalsAiRtdProvider.js b/modules/nodalsAiRtdProvider.js index ac2900f6c7b..8cf0df0bde2 100644 --- a/modules/nodalsAiRtdProvider.js +++ b/modules/nodalsAiRtdProvider.js @@ -55,7 +55,7 @@ class NodalsAiRtdProvider { const params = config?.params || {}; if ( this.#isValidConfig(params) && - this.#hasRequiredUserConsent(userConsent) + this.#hasRequiredUserConsent(userConsent, config) ) { this.#propertyId = params.propertyId; this.#userConsent = userConsent; @@ -82,7 +82,7 @@ class NodalsAiRtdProvider { */ getTargetingData(adUnitArray, config, userConsent) { let targetingData = {}; - if (!this.#hasRequiredUserConsent(userConsent)) { + if (!this.#hasRequiredUserConsent(userConsent, config)) { return targetingData; } this.#userConsent = userConsent; @@ -104,7 +104,7 @@ class NodalsAiRtdProvider { } getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - if (!this.#hasRequiredUserConsent(userConsent)) { + if (!this.#hasRequiredUserConsent(userConsent, config)) { callback(); return; } @@ -133,7 +133,7 @@ class NodalsAiRtdProvider { } onBidResponseEvent(bidResponse, config, userConsent) { - if (!this.#hasRequiredUserConsent(userConsent)) { + if (!this.#hasRequiredUserConsent(userConsent, config)) { return; } this.#userConsent = userConsent; @@ -154,7 +154,7 @@ class NodalsAiRtdProvider { } onAuctionEndEvent(auctionDetails, config, userConsent) { - if (!this.#hasRequiredUserConsent(userConsent)) { + if (!this.#hasRequiredUserConsent(userConsent, config)) { return; } this.#userConsent = userConsent; @@ -240,11 +240,12 @@ class NodalsAiRtdProvider { /** * Checks if the user has provided the required consent. * @param {Object} userConsent - User consent object. + * @param {Object} config - Configuration object for the module. * @returns {boolean} - True if the user consent is valid, false otherwise. */ - #hasRequiredUserConsent(userConsent) { - if (!userConsent.gdpr || userConsent.gdpr?.gdprApplies === false) { + #hasRequiredUserConsent(userConsent, config) { + if (config?.params?.publisherProvidedConsent === true || !userConsent.gdpr || userConsent.gdpr?.gdprApplies === false) { return true; } if ( diff --git a/modules/nubaBidAdapter.js b/modules/nubaBidAdapter.js new file mode 100644 index 00000000000..0ebfe715508 --- /dev/null +++ b/modules/nubaBidAdapter.js @@ -0,0 +1,19 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { isBidRequestValid, buildRequests, interpretResponse } from '../libraries/teqblazeUtils/bidderUtils.js'; + +const BIDDER_CODE = 'nuba'; + +const AD_URL = 'https://ads.nuba.io/openrtb2/auction'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: isBidRequestValid(), + buildRequests: buildRequests(AD_URL), + interpretResponse, + getUserSyncs: () => {}, +}; + +registerBidder(spec); diff --git a/modules/nubaBidAdapter.md b/modules/nubaBidAdapter.md new file mode 100644 index 00000000000..6f1500e6ab1 --- /dev/null +++ b/modules/nubaBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Nuba Bidder Adapter +Module Type: Nuba Bidder Adapter +Maintainer: ssp@nuba.io +``` + +# Description + +Connects to Nuba.io exchange for bids. +Nuba.io bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'nuba', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'nuba', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'nuba', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/omnidexBidAdapter.js b/modules/omnidexBidAdapter.js index a72234bd521..6fd1f34cf21 100644 --- a/modules/omnidexBidAdapter.js +++ b/modules/omnidexBidAdapter.js @@ -12,6 +12,7 @@ import { const DEFAULT_SUB_DOMAIN = 'exchange'; const BIDDER_CODE = 'omnidex'; const BIDDER_VERSION = '1.0.0'; +const GVLID = 1463; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { @@ -41,7 +42,8 @@ export const spec = { buildRequests, interpretResponse, getUserSyncs, - onBidWon + onBidWon, + gvlid: GVLID, }; registerBidder(spec); diff --git a/modules/omsBidAdapter.js b/modules/omsBidAdapter.js index 289a763a8ac..68d93a123f7 100644 --- a/modules/omsBidAdapter.js +++ b/modules/omsBidAdapter.js @@ -51,18 +51,21 @@ function buildRequests(bidReqs, bidderRequest) { const imp = { id: bid.bidId, - banner: { - format: processedSizes, - ext: { - viewability: viewabilityAmountRounded, - } - }, ext: { ...gpidData }, tagid: String(bid.adUnitCode) }; + if (bid?.mediaTypes?.banner) { + imp.banner = { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded, + } + } + } + if (bid?.mediaTypes?.video) { imp.video = { ...bid.mediaTypes.video, diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js index 22b8476ef54..2bcacd92dd9 100644 --- a/modules/ooloAnalyticsAdapter.js +++ b/modules/ooloAnalyticsAdapter.js @@ -22,6 +22,7 @@ const prebidVersion = '$prebid.version$' const analyticsType = 'endpoint' const ADAPTER_CODE = 'oolo' const AUCTION_END_SEND_TIMEOUT = 1500 +// TODO: consider using the Prebid-generated page view ID instead of generating a custom one export const PAGEVIEW_ID = +generatePageViewId() const { diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index c2382ed82f7..f82e0337e7f 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, deepSetValue, diff --git a/modules/opscoBidAdapter.js b/modules/opscoBidAdapter.js index 60cf32dc7a9..835387d9bc1 100644 --- a/modules/opscoBidAdapter.js +++ b/modules/opscoBidAdapter.js @@ -1,6 +1,8 @@ import {deepAccess, deepSetValue, isArray, logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js'; const ENDPOINT = 'https://exchange.ops.co/openrtb2/auction'; const BIDDER_CODE = 'opsco'; @@ -8,71 +10,101 @@ const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER], +const converter = ortbConverter({ + request(buildRequest, imps, bidderRequest, context) { + const {bidRequests} = context; + const data = buildRequest(imps, bidderRequest, context); - isBidRequestValid: (bid) => !!(bid.params && - bid.params.placementId && - bid.params.publisherId && - bid.mediaTypes?.banner?.sizes && - Array.isArray(bid.mediaTypes?.banner?.sizes)), + const {publisherId, siteId} = bidRequests[0].params; - buildRequests: (validBidRequests, bidderRequest) => { - if (!validBidRequests || !bidderRequest) { - return; - } + data.site = data.site || {}; + data.site.id = siteId; - const {publisherId, siteId} = validBidRequests[0].params; - - const payload = { - id: bidderRequest.bidderRequestId, - imp: validBidRequests.map(bidRequest => ({ - id: bidRequest.bidId, - banner: {format: extractSizes(bidRequest)}, - ext: { - opsco: { - placementId: bidRequest.params.placementId, - publisherId: publisherId, - } - } - })), - site: { - id: siteId, - publisher: {id: publisherId}, - domain: bidderRequest.refererInfo?.domain, - page: bidderRequest.refererInfo?.page, - ref: bidderRequest.refererInfo?.ref, - }, - }; + data.site.publisher = data.site.publisher || {}; + data.site.publisher.id = publisherId; + + data.site.domain = data.site.domain || bidderRequest.refererInfo?.domain; + data.site.page = data.site.page || bidderRequest.refererInfo?.page; + data.site.ref = data.site.ref || bidderRequest.refererInfo?.ref; - if (isTest(validBidRequests[0])) { - payload.test = 1; + if (isTest(bidRequests[0])) { + data.test = 1; + } else { + delete data.test; } + imps.forEach(imp => { + delete imp.ext.opsco.test; + }); + if (bidderRequest.gdprConsent) { - deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(payload, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(data, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + if (bidderRequest.uspConsent) { + deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); + + const eids = deepAccess(bidRequests[0], 'userIdAsEids'); if (eids && eids.length !== 0) { - deepSetValue(payload, 'user.ext.eids', eids); + deepSetValue(data, 'user.ext.eids', eids); } - const schain = validBidRequests[0]?.ortb2?.source?.ext?.schain; + const schain = bidRequests[0]?.ortb2?.source?.ext?.schain; const schainData = schain?.nodes; if (isArray(schainData) && schainData.length > 0) { - deepSetValue(payload, 'source.ext.schain', schain); + deepSetValue(data, 'source.ext.schain', schain); } - if (bidderRequest.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + return data; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + imp.ext.opsco = imp.ext.prebid.bidder.opsco; + delete imp.ext.prebid.bidder; + + if (!imp.bidfloor && bidRequest.params?.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; } + return imp; + }, + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY + }, + processors: pbsExtensions +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => !!(bid.params && + bid.params.placementId && + bid.params.publisherId && + bid.mediaTypes?.banner?.sizes && + Array.isArray(bid.mediaTypes?.banner?.sizes)), + + buildRequests: (validBidRequests, bidderRequest) => { + if (!validBidRequests || !bidderRequest) { + return; + } + + const data = converter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: validBidRequests, + context: { mediaType: BANNER } + }); + return { method: 'POST', url: ENDPOINT, - data: JSON.stringify(payload), + data: data, }; }, @@ -123,10 +155,6 @@ export const spec = { } }; -function extractSizes(bidRequest) { - return (bidRequest.mediaTypes?.banner?.sizes || []).map(([width, height]) => ({w: width, h: height})); -} - function isTest(validBidRequest) { return validBidRequest.params?.test === true; } diff --git a/modules/optableRtdProvider.md b/modules/optableRtdProvider.md index 1250437f6f0..45fc7d589d7 100644 --- a/modules/optableRtdProvider.md +++ b/modules/optableRtdProvider.md @@ -6,6 +6,10 @@ Module Type: RTD Provider Maintainer: prebid@optable.co +## Minimal Prebid.js Versions + +Prebid.js minimum version: 9.53.2+, or 10.2+ + ## Description Optable RTD submodule enriches the OpenRTB request by populating `user.ext.eids` and `user.data` using an identity graph and audience segmentation service hosted by Optable on behalf of the publisher. This RTD submodule primarily relies on the Optable bundle loaded on the page, which leverages the Optable-specific Visitor ID and other PPIDs to interact with the identity graph, enriching the bid request with additional user IDs and audience data. @@ -30,23 +34,18 @@ In order to use the module you first need to register with Optable and obtain a ``` -In this case bundleUrl parameter is not needed and the script will await bundle loading before delegating to it. - ### Configuration -This module is configured as part of the `realTimeData.dataProviders`. We recommend setting `auctionDelay` to 1000 ms and make sure `waitForIt` is set to `true` for the `Optable` RTD provider. +This module is configured as part of the `realTimeData.dataProviders`. ```javascript pbjs.setConfig({ debug: true, // we recommend turning this on for testing as it adds more logging realTimeData: { - auctionDelay: 1000, dataProviders: [ { name: 'optable', - waitForIt: true, // should be true, otherwise the auctionDelay will be ignored params: { - bundleUrl: '', adserverTargeting: '', }, }, @@ -55,48 +54,12 @@ pbjs.setConfig({ }); ``` -### Additional input to the module - -Optable bundle may use PPIDs (publisher provided IDs) from the `user.ext.eids` as input. - -In addition, other arbitrary keys can be used as input, f.e. the following: - -- `optableRtdConfig.email` - a SHA256-hashed user email -- `optableRtdConfig.phone` - a SHA256-hashed [E.164 normalized phone](https://unifiedid.com/docs/getting-started/gs-normalization-encoding#phone-number-normalization) (meaning a phone number consisting of digits and leading plus sign without spaces or any additional characters, f.e. a US number would be: `+12345678999`) -- `optableRtdConfig.postal_code` - a ZIP postal code string - -Each of these identifiers is completely optional and can be provided through `pbjs.mergeConfig(...)` like so: - -```javascript -pbjs.mergeConfig({ - optableRtdConfig: { - email: await sha256("test@example.com"), - phone: await sha256("12345678999"), - postal_code: "61054" - } -}) -``` - -Where `sha256` function can be defined as: - -```javascript -async function sha256(input) { - return [...new Uint8Array( - await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input)) - )].map(b => b.toString(16).padStart(2, "0")).join(""); -} -``` - -To handle PPIDs and the above input - a custom `handleRtd` function may need to be provided. - ### Parameters | Name | Type | Description | Default | Notes | |--------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------|----------| | name | String | Real time data module name | Always `optable` | | -| waitForIt | Boolean | Should be set `true` together with `auctionDelay: 1000` | `false` | | | params | Object | | | | -| params.bundleUrl | String | Optable bundle URL | `null` | Optional | | params.adserverTargeting | Boolean | If set to `true`, targeting keywords will be passed to the ad server upon auction completion | `true` | Optional | | params.handleRtd | Function | An optional function that uses Optable data to enrich `reqBidsConfigObj` with the real-time data. If not provided, the module will do a default call to Optable bundle. The function signature is `[async] (reqBidsConfigObj, optableExtraData, mergeFn) => {}` | `null` | Optional | diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index 3a46184e9c1..153fbea2980 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -56,9 +56,6 @@ export let transmitWithBidRequests = 'allow'; /** @type {Object} */ export let optimeraTargeting = {}; -/** @type {boolean} */ -export let fetchScoreFile = true; - /** @type {RtdSubmodule} */ export const optimeraSubmodule = { name: 'optimeraRTD', @@ -84,7 +81,6 @@ export function init(moduleConfig) { if (_moduleParams.transmitWithBidRequests) { transmitWithBidRequests = _moduleParams.transmitWithBidRequests; } - setScoresURL(); return true; } logError('Optimera clientID is missing in the Optimera RTD configuration.'); @@ -111,9 +107,9 @@ export function setScoresURL() { if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; - fetchScoreFile = true; + return true; } else { - fetchScoreFile = false; + return false; } } @@ -125,6 +121,12 @@ export function setScoresURL() { * @param {object} userConsent */ export function fetchScores(reqBidsConfigObj, callback, config, userConsent) { + // If setScoresURL returns false, no need to re-fetch the score file + if (!setScoresURL()) { + callback(); + return; + } + // Else, fetch the score file const ajax = ajaxBuilder(); ajax(scoresURL, { success: (res, req) => { diff --git a/modules/permutiveIdentityManagerIdSystem.js b/modules/permutiveIdentityManagerIdSystem.js index 5dc12d44edb..f3849644445 100644 --- a/modules/permutiveIdentityManagerIdSystem.js +++ b/modules/permutiveIdentityManagerIdSystem.js @@ -10,6 +10,7 @@ import {prefixLog, safeJSONParse} from '../src/utils.js' */ const MODULE_NAME = 'permutiveIdentityManagerId' +const PERMUTIVE_GVLID = 361 const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id' const ID5_DOMAIN = 'id5-sync.com' @@ -80,6 +81,7 @@ export const permutiveIdentityManagerIdSubmodule = { * @type {string} */ name: MODULE_NAME, + gvlid: PERMUTIVE_GVLID, /** * decode the stored id value for passing to bid requests diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index bb06d2d138e..886dc8b3b5f 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -17,6 +17,7 @@ import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; */ const MODULE_NAME = 'permutive' +const PERMUTIVE_GVLID = 361 const logger = prefixLog('[PermutiveRTD]') @@ -466,6 +467,7 @@ let permutiveSDKInRealTime = false /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, + gvlid: PERMUTIVE_GVLID, getBidRequestData: function (reqBidsConfigObj, callback, customModuleConfig) { const completeBidRequestData = () => { logger.logInfo(`Request data updated`) diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js index 36dbd1159cc..859bfc9de7e 100644 --- a/modules/pgamsspBidAdapter.js +++ b/modules/pgamsspBidAdapter.js @@ -3,13 +3,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isBidRequestValid, buildRequests, interpretResponse, getUserSyncs } from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'pgamssp'; -const GVLID = 1353; const AD_URL = 'https://us-east.pgammedia.com/pbjs'; const SYNC_URL = 'https://cs.pgammedia.com'; export const spec = { code: BIDDER_CODE, - gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: isBidRequestValid(), diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 7bf1602e5e7..ab9e18e6837 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -27,10 +27,10 @@ const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', const getMinimumFloor = (() => { const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); return function(candidates) { - let min; + let min = null; for (const candidate of candidates) { if (candidate?.bidfloorcur == null || candidate?.bidfloor == null) return null; - min = min == null ? candidate : getMin(min, candidate); + min = min === null ? candidate : getMin(min, candidate); } return min; } @@ -133,7 +133,7 @@ const PBS_CONVERTER = ortbConverter({ // also, take overrides from s2sConfig.adapterOptions const adapterOptions = context.s2sBidRequest.s2sConfig.adapterOptions; for (const req of context.actualBidRequests.values()) { - setImpBidParams(imp, req, context, context); + setImpBidParams(imp, req); if (adapterOptions && adapterOptions[req.bidder]) { Object.assign(imp.ext.prebid.bidder[req.bidder], adapterOptions[req.bidder]); } @@ -237,6 +237,10 @@ const PBS_CONVERTER = ortbConverter({ extPrebidAliases(orig, ortbRequest, proxyBidderRequest, context) { // override alias processing to do it for each bidder in the request context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); + }, + extPrebidPageViewIds(orig, ortbRequest, proxyBidderRequest, context) { + // override page view ID processing to do it for each bidder in the request + context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); } }, [RESPONSE]: { diff --git a/modules/publicGoodBidAdapter.js b/modules/publicGoodBidAdapter.js new file mode 100644 index 00000000000..b5fa56d7a53 --- /dev/null +++ b/modules/publicGoodBidAdapter.js @@ -0,0 +1,83 @@ +'use strict'; + +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'publicgood'; +const PUBLIC_GOOD_ENDPOINT = 'https://advice.pgs.io'; +var PGSAdServed = false; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE], + + isBidRequestValid: function (bid) { + if (PGSAdServed || !bid.params.partnerId || !bid.params.slotId) { + return false; + } + + return true; + }, + + buildRequests: function (validBidRequests, bidderRequest) { + let partnerId = ""; + let slotId = ""; + + if (validBidRequests[0] && validBidRequests[0].params) { + partnerId = validBidRequests[0].params.partnerId; + slotId = validBidRequests[0].params.slotId; + } + + let payload = { + url: bidderRequest.refererInfo.page || bidderRequest.refererInfo.referer, + partner_id: partnerId, + isprebid: true, + slotid: slotId, + bidRequest: validBidRequests[0] + } + + return { + method: 'POST', + url: PUBLIC_GOOD_ENDPOINT, + data: payload, + options: { + withCredentials: false, + }, + bidId: validBidRequests[0].bidId + } + }, + + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + let bidResponses = []; + let bidResponse = {}; + let partnerId = serverBody && serverBody.targetData ? serverBody.targetData.partner_id : "error"; + + if (!serverBody || typeof serverBody !== 'object') { + return []; + } + + if (serverBody.action !== 'Hide' && !PGSAdServed) { + bidResponse.requestId = bidRequest.bidId; + bidResponse.creativeId = serverBody.targetData.target_id; + bidResponse.cpm = serverBody.targetData.cpm; + bidResponse.width = 320; + bidResponse.height = 470; + bidResponse.ad = `
`; + bidResponse.currency = 'USD'; + bidResponse.netRevenue = true; + bidResponse.ttl = 360; + bidResponse.meta = {advertiserDomains: []}; + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + onBidWon: function(bid) { + // Win once per page load + PGSAdServed = true; + } + +}; +registerBidder(spec); diff --git a/modules/publicGoodBigAdapter.md b/modules/publicGoodBigAdapter.md new file mode 100644 index 00000000000..09fb0879edf --- /dev/null +++ b/modules/publicGoodBigAdapter.md @@ -0,0 +1,33 @@ +# Overview + +**Module Name**: Public Good Bidder Adapter\ +**Module Type**: Bidder Adapter\ +**Maintainer**: publicgood@publicgood.com + +# Description + +Public Good's bid adapter is for use with approved publishers only. Any publisher who wishes to integrate with Pubic Good using the this adapter will need a partner ID. + +Please contact Public Good for additional information and a negotiated set of slots. + +# Test Parameters +``` +{ + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + } +} +``` + +# Publisher Parameters +``` +{ + bidder: 'publicgood', + params: { + partnerId: '-- partner ID provided by public good --', + slotId: 'all | -- optional slot identifier --' + } +} +``` \ No newline at end of file diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 8b8fb74e74c..7f2abeb5b6a 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -505,7 +505,7 @@ const updateResponseWithCustomFields = (res, bid, ctx) => { } const addExtenstionParams = (req, bidderRequest) => { - const { profId, verId, wiid, transactionId } = conf; + const { profId, verId, wiid } = conf; req.ext = { epoch: new Date().getTime(), // Sending epoch timestamp in request.ext object wrapper: { @@ -513,7 +513,6 @@ const addExtenstionParams = (req, bidderRequest) => { version: verId ? parseInt(verId) : undefined, wiid: wiid, wv: '$$REPO_AND_VERSION$$', - transactionId, wp: 'pbjs', biddercode: bidderRequest?.bidderCode }, @@ -827,7 +826,6 @@ export const spec = { originalBid.params.wiid = originalBid.params.wiid || bidderRequest.auctionId || wiid; bid = deepClone(originalBid); _handleCustomParams(bid.params, conf); - conf.transactionId = bid.ortb2Imp?.ext?.tid; const { bcat, acat } = bid.params; if (bcat) { blockedIabCategories = blockedIabCategories.concat(bcat); diff --git a/modules/pwbidBidAdapter.js b/modules/pwbidBidAdapter.js index 39c4c70c834..cb383fee630 100644 --- a/modules/pwbidBidAdapter.js +++ b/modules/pwbidBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { _each, isBoolean, isNumber, isStr, deepClone, isArray, deepSetValue, inIframe, mergeDeep, deepAccess, logMessage, logInfo, logWarn, logError, isPlainObject } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; diff --git a/modules/rhythmoneBidAdapter.js b/modules/rhythmoneBidAdapter.js index 2d9af775562..e3d4b15aebc 100644 --- a/modules/rhythmoneBidAdapter.js +++ b/modules/rhythmoneBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 7d1db618780..4d61d827650 100644 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -291,7 +291,9 @@ function raiGetResolution() { function raiSetPbAdSlot(bid) { let pbAdSlot = ''; - if (deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') != null) { + if (deepAccess(bid, 'ortb2Imp.ext.gpid') != null) { + pbAdSlot = deepAccess(bid, 'ortb2Imp.ext.gpid') + } else if (deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') != null) { pbAdSlot = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot') } return pbAdSlot diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 83536bd0abe..9aec645b715 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, deepClone, isArray, logError, mergeDeep, isEmpty, isPlainObject, isNumber, isStr} from '../src/utils.js'; +import {deepAccess, deepClone, isArray, logError, mergeDeep, isEmpty, isPlainObject, isNumber, isStr, deepSetValue} from '../src/utils.js'; import {getOrigin} from '../libraries/getOrigin/index.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -87,6 +87,14 @@ export const spec = { }); } + if (bidderRequest.gppConsent?.gppString) { + deepSetValue(request, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(request, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (ortb2Params.regs?.gpp) { + deepSetValue(request, 'regs.gpp', ortb2Params.regs.gpp); + deepSetValue(request, 'regs.gpp_sid', ortb2Params.regs.gpp_sid); + } + const computedEndpointUrl = ENDPOINT_URL; return { @@ -135,18 +143,32 @@ registerBidder(spec); /** * @param {object} slot Ad Unit Params by Prebid - * @returns {number} floor by imp type + * @returns {number|null} floor value, or null if not available */ function applyFloor(slot) { - const floors = []; + // If Price Floors module is available, use it if (typeof slot.getFloor === 'function') { - Object.keys(slot.mediaTypes).forEach(type => { - if (SUPPORTED_MEDIA_TYPES.includes(type)) { - floors.push(slot.getFloor({ currency: DEFAULT_CURRENCY_ARR[0], mediaType: type, size: slot.sizes || '*' })?.floor); + try { + const floor = slot.getFloor({ + currency: DEFAULT_CURRENCY_ARR[0], + mediaType: '*', + size: '*' + }); + + if (floor && floor.currency === DEFAULT_CURRENCY_ARR[0] && !isNaN(parseFloat(floor.floor))) { + return floor.floor; } - }); + } catch (e) { + logError('RTB House: Error calling getFloor:', e); + } + } + + // Fallback to bidfloor param if available + if (slot.params.bidfloor && !isNaN(parseFloat(slot.params.bidfloor))) { + return parseFloat(slot.params.bidfloor); } - return floors.length > 0 ? Math.max(...floors) : parseFloat(slot.params.bidfloor); + + return null; } /** diff --git a/modules/rtdModule/index.ts b/modules/rtdModule/index.ts index 3cd689a94f2..73473b6d163 100644 --- a/modules/rtdModule/index.ts +++ b/modules/rtdModule/index.ts @@ -156,7 +156,6 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest let callbacksExpected = prioritySubModules.length; let isDone = false; let waitTimeout; - const verifiers = []; if (!relevantSubModules.length) { return exitHook(); @@ -167,9 +166,7 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest relevantSubModules.forEach(sm => { const fpdGuard = guardOrtb2Fragments(reqBidsConfigObj.ortb2Fragments || {}, activityParams(MODULE_TYPE_RTD, sm.name)); - verifiers.push(fpdGuard.verify); - reqBidsConfigObj.ortb2Fragments = fpdGuard.obj; - sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent, timeout); + sm.getBidRequestData({...reqBidsConfigObj, ortb2Fragments: fpdGuard}, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent, timeout); }); function onGetBidRequestDataCallback() { @@ -190,7 +187,6 @@ export const setBidRequestsData = timedAuctionHook('rtd', function setBidRequest } isDone = true; clearTimeout(waitTimeout); - verifiers.forEach(fn => fn()); fn.call(this, reqBidsConfigObj); } }); diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 6543a6a88e1..76e973c7527 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -427,7 +427,6 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', - 'o_ae', 'o_cdep', 'rp_floor', 'rp_secure', @@ -545,9 +544,6 @@ export const spec = { data['ppuid'] = configUserId; } - if (bidRequest?.ortb2Imp?.ext?.ae) { - data['o_ae'] = 1; - } // If the bid request contains a 'mobile' property under 'ortb2.site', add it to 'data' as 'p_site.mobile'. if (typeof bidRequest?.ortb2?.site?.mobile === 'number') { data['p_site.mobile'] = bidRequest.ortb2.site.mobile @@ -655,7 +651,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which + * @return {*} An array of bids */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -760,15 +756,7 @@ export const spec = { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); - const fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { - return { config, bidId: config.bidId } - }); - - if (fledgeAuctionConfigs) { - return { bids, paapi: fledgeAuctionConfigs }; - } else { - return bids; - } + return bids; }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (syncOptions.iframeEnabled) { diff --git a/modules/scaliburBidAdapter.js b/modules/scaliburBidAdapter.js new file mode 100644 index 00000000000..b8b05c3ac61 --- /dev/null +++ b/modules/scaliburBidAdapter.js @@ -0,0 +1,241 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {sizesToSizeTuples} from "../src/utils.js"; + +const BIDDER_CODE = 'scalibur'; +const ENDPOINT_SERVER = new URLSearchParams(window.location.search).get('sclServer') || 'srv'; +const ENDPOINT_URL = `https://${ENDPOINT_SERVER}.scalibur.io/adserver/ortb?type=prebid`; +const SYNC_IFRAME_URL = `https://${ENDPOINT_SERVER}.scalibur.io/adserver/sync`; +const SYNC_PIXEL_URL = `https://${ENDPOINT_SERVER}.scalibur.io/adserver/sync`; +const DEFAULT_CURRENCY = 'USD'; +const BIDDER_VERSION = '1.0.0'; +const IFRAME_TYPE_Q_PARAM = 'iframe'; +const IMAGE_TYPE_Q_PARAM = 'img'; +const GVLID = 1471; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const STORAGE_KEY = `${BIDDER_CODE}_fp_data`; + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function (bid) { + return !!(bid.params && bid.params.placementId); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const ortb2 = bidderRequest.ortb2 || {}; + const ortb2Site = ortb2.site || {}; + const ortb2User = ortb2.user || {}; + const ortb2Regs = ortb2.regs || {}; + const ortb2Device = ortb2.device || {}; + const ortb2SourceExt = ortb2.source?.ext || {}; + const eids = ortb2User?.ext?.eids || []; + const fpd = getFirstPartyData(); + + const payload = { + id: bidderRequest.auctionId, + imp: validBidRequests.map((bid) => { + const imp = { + id: bid.bidId, + ext: { + placementId: bid.params.placementId, + adUnitCode: bid.adUnitCode, + ...bid.params, + }, + }; + + // Banner Media Type + if (bid.mediaTypes.banner) { + imp.banner = { + format: sizesToSizeTuples(bid.mediaTypes.banner.sizes).map((size) => ({ + w: size[0], + h: size[1], + })), + }; + } + + // Video Media Type + if (bid.mediaTypes.video) { + const video = bid.mediaTypes.video; + imp.video = { + mimes: video.mimes || ['video/mp4'], + minduration: video.minduration || 1, + maxduration: video.maxduration || 180, + maxbitrate: video.maxbitrate || 30000, + protocols: video.protocols || [2, 3, 5, 6], + w: video.playerSize?.[0]?.[0] || 640, + h: video.playerSize?.[0]?.[1] || 480, + placement: video.placement || 1, + plcmt: video.plcmt || 1, + skip: video.skip || 0, + skipafter: video.skipafter || 5, + startdelay: video.startdelay || 0, + playbackmethod: video.playbackmethod || [1, 2], + api: video.api || [1, 2], + linearity: video.linearity || 1, + }; + + // OMID Params + if (video.api && video.api.includes(7)) { + if (ortb2SourceExt.omidpn) { + imp.video.omidpn = ortb2SourceExt.omidpn; + } + if (ortb2SourceExt.omidpv) { + imp.video.omidpv = ortb2SourceExt.omidpv; + } + } + } + + // Floor Price + const floor = bid.getFloor ? bid.getFloor({currency: DEFAULT_CURRENCY, mediaType: '*', size: '*'}) : {}; + imp.bidfloor = floor.floor || bid.params.bidfloor || 0; + imp.bidfloorcur = floor.currency || bid.params.bidfloorcur || DEFAULT_CURRENCY; + + // GPID + if (bid.ortb2Imp?.ext?.gpid) { + imp.ext.gpid = bid.ortb2Imp.ext.gpid; + } + + return imp; + }), + site: { + page: bidderRequest.refererInfo.page, + domain: bidderRequest.refererInfo.domain, + ref: ortb2Site.ref || '', + keywords: ortb2Site.keywords || '', + pagecat: ortb2Site.pagecat || [], + content: ortb2Site.content || {}, + }, + device: { + ua: ortb2Device.ua, + language: ortb2Device.language, + sua: ortb2Device.sua || {}, + dnt: ortb2Device.dnt ?? 0, + }, + user: { + eids, + consent: bidderRequest.gdprConsent?.consentString || '', + data: ortb2User.data || [], + }, + regs: { + coppa: ortb2Regs.coppa || 0, + gdpr: bidderRequest.gdprConsent?.gdprApplies ? 1 : 0, + us_privacy: bidderRequest.uspConsent || '', + gpp: bidderRequest.gppConsent?.gppString || '', + gpp_sid: bidderRequest.gppConsent?.applicableSections || [], + ext: { + gpc: ortb2Regs.ext?.gpc || '', + }, + }, + source: { + tid: bidderRequest.auctionId, + }, + tmax: bidderRequest.timeout, + ext: { + prebidVersion: '$prebid.version$', + bidderVersion: BIDDER_VERSION, + isDebug: config.getConfig('debug'), + ...fpd + } + }; + + // Supply Chain + if (validBidRequests[0]?.ortb2?.source?.ext?.schain) { + payload.source.schain = validBidRequests[0]?.ortb2?.source?.ext?.schain; + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload + }; + }, + + interpretResponse: function (serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + + const bidResponses = []; + const response = serverResponse.body; + + if (response && response.seatbid) { + response.seatbid.forEach((seat) => { + seat.bid.forEach((bid) => { + const imp = request.data.imp.find((i) => i.id === bid.impid); + let bidRes = { + requestId: bid.impid, + cpm: bid.cpm, + width: bid.width, + height: bid.height, + creativeId: bid.crid || '', + currency: response.cur || DEFAULT_CURRENCY, + netRevenue: true, + ttl: bid.exp || 300, + }; + if (imp && imp.banner) { + bidRes.ad = bid.adm; + bidRes.mediaType = BANNER; + } else if (imp && imp.video) { + bidRes.vastXml = bid.vastXml; + bidRes.vastUrl = bid.vastUrl; + bidRes.mediaType = VIDEO; + } + + bidResponses.push(bidRes); + }); + }); + } + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const gdpr = gdprConsent?.gdprApplies ? 1 : 0; + const gdprConsentString = gdprConsent?.consentString || ''; + const gpp = gdprConsent?.gppString || ''; + const gppSid = gdprConsent?.applicableSections || []; + const usPrivacy = uspConsent || ''; + + const queryParams = [ + `type=${syncOptions.iframeEnabled ? IFRAME_TYPE_Q_PARAM : (syncOptions.pixelEnabled ? IMAGE_TYPE_Q_PARAM : '')}`, + `gdpr=${gdpr}`, + `gdpr_consent=${encodeURIComponent(gdprConsentString)}`, + `us_privacy=${encodeURIComponent(usPrivacy)}`, + `gpp=${encodeURIComponent(gpp)}`, + `gpp_sid=${encodeURIComponent(gppSid.join(','))}`, + ].join('&'); + + const syncs = []; + if (syncOptions.iframeEnabled) { + syncs.push({type: 'iframe', url: `${SYNC_IFRAME_URL}?${queryParams}`}); + } + if (syncOptions.pixelEnabled) { + syncs.push({type: 'image', url: `${SYNC_PIXEL_URL}?${queryParams}`}); + } + return syncs; + }, +}; + +// Also, export storage for easier testing. +export { storage }; + +export function getFirstPartyData() { + if (!storage.hasLocalStorage()) return; + + let rawData = storage.getDataFromLocalStorage(STORAGE_KEY); + let fdata = null; + if (rawData) { + try { + fdata = JSON.parse(rawData); + } catch (e) {} + } + + return fdata || {}; +} + +registerBidder(spec); diff --git a/modules/scaliburBidAdapter.md b/modules/scaliburBidAdapter.md new file mode 100644 index 00000000000..1ad7b5613b7 --- /dev/null +++ b/modules/scaliburBidAdapter.md @@ -0,0 +1,91 @@ +# Overview + +**Module Name**: Scalibur Bid Adapter +**Module Type**: Bidder Adapter +**Maintainer**: support@scalibur.io +**GVLID**: 1471 + +# Description + +The Scalibur Bid Adapter connects publishers to Scalibur's programmatic advertising platform. It supports both banner and video ad formats through OpenRTB 2.x protocol and provides full compliance with privacy regulations. + +**Key Features:** +- Banner and Video ad support +- OpenRTB 2.x compliant +- Privacy regulation compliance +- Floor pricing support +- User sync capabilities +- Supply chain transparency + +# Test Parameters + +## Banner + +```javascript +var adUnits = [ + { + code: 'test-banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, + bids: [ + { + bidder: 'scalibur', + params: { + placementId: 'test-scl-placement' // Required + } + } + ] + } +]; +``` + +## Video + +```javascript +var adUnits = [ + { + code: 'test-video-div', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream', + mimes: ['video/mp4'], + protocols: [2, 3, 5, 6], + minduration: 5, + maxduration: 30, + startdelay: 0, + playbackmethod: [1, 2], + api: [1, 2] + } + }, + bids: [ + { + bidder: 'scalibur', + params: { + placementId: 'test-scl-placement' // Required + } + } + ] + } +]; +``` + +# Configuration +## Required Parameters + +| Name | Scope | Description | Example | Type | +| --- | --- | --- | --- | --- | +| `placementId` | required | Placement identifier provided by Scalibur | `'test-placement-123'` | `string` | + +# Additional Information + +## User Syncs +The adapter supports both iframe and image-based user syncs: +- **Iframe sync** +- **Image sync** + +All privacy parameters are automatically included in sync URLs. + diff --git a/modules/screencoreBidAdapter.js b/modules/screencoreBidAdapter.js index ac6f5895751..22f71cf1379 100644 --- a/modules/screencoreBidAdapter.js +++ b/modules/screencoreBidAdapter.js @@ -1,16 +1,17 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; import { - createBuildRequestsFn, - createInterpretResponseFn, - createUserSyncGetter, isBidRequestValid, -} from '../libraries/vidazooUtils/bidderUtils.js'; + buildRequestsBase, + interpretResponse, + getUserSyncs, + buildPlacementProcessingFunction +} from '../libraries/teqblazeUtils/bidderUtils.js'; const BIDDER_CODE = 'screencore'; const GVLID = 1473; const BIDDER_VERSION = '1.0.0'; +const SYNC_URL = 'https://cs.screencore.io'; const REGION_SUBDOMAIN_SUFFIX = { EU: 'taqeu', US: 'taqus', @@ -48,32 +49,29 @@ function getRegionSubdomainSuffix() { } } -export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); - export function createDomain() { const subDomain = getRegionSubdomainSuffix(); return `https://${subDomain}.screencore.io`; } -const buildRequests = createBuildRequestsFn(createDomain, null, storage, BIDDER_CODE, BIDDER_VERSION, false); +const AD_URL = `${createDomain()}/prebid`; -const interpretResponse = createInterpretResponseFn(BIDDER_CODE, false); +const placementProcessingFunction = buildPlacementProcessingFunction(); -const getUserSyncs = createUserSyncGetter({ - iframeSyncUrl: 'https://cs.screencore.io/api/sync/iframe', - imageSyncUrl: 'https://cs.screencore.io/api/sync/image', -}); +const buildRequests = (validBidRequests = [], bidderRequest = {}) => { + return buildRequestsBase({ adUrl: AD_URL, validBidRequests, bidderRequest, placementProcessingFunction }); +}; export const spec = { code: BIDDER_CODE, version: BIDDER_VERSION, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid, + isBidRequestValid: isBidRequestValid(), buildRequests, interpretResponse, - getUserSyncs, + getUserSyncs: getUserSyncs(SYNC_URL), }; registerBidder(spec); diff --git a/modules/screencoreBidAdapter.md b/modules/screencoreBidAdapter.md index 60dc9b9ab21..8e5d9e3d3da 100644 --- a/modules/screencoreBidAdapter.md +++ b/modules/screencoreBidAdapter.md @@ -27,7 +27,8 @@ var adUnits = [ param1: 'loremipsum', param2: 'dolorsitamet' }, - placementId: 'testBanner' + placementId: 'testBanner', + endpointId: 'testEndpoint' } } ] diff --git a/modules/seenthisBrandStories.md b/modules/seenthisBrandStories.md new file mode 100644 index 00000000000..8b438d8ca6c --- /dev/null +++ b/modules/seenthisBrandStories.md @@ -0,0 +1,10 @@ +# Overview + +Module Name: SeenThis Brand Stories +Maintainer: tech@seenthis.se + +# Description + +Module to allow publishers to handle SeenThis Brand Stories ads. The module will handle communication with the ad iframe and resize the ad iframe accurately and handle fullscreen mode according to product specification. + +This will allow publishers to safely run the ad format without the need to disable Safeframe when using Prebid.js. diff --git a/modules/seenthisBrandStories.ts b/modules/seenthisBrandStories.ts new file mode 100644 index 00000000000..8c6ee589272 --- /dev/null +++ b/modules/seenthisBrandStories.ts @@ -0,0 +1,161 @@ +import { EVENTS } from "../src/constants.js"; +import { getBoundingClientRect } from "../libraries/boundingClientRect/boundingClientRect.js"; +import { getWinDimensions } from "../src/utils.js"; +import * as events from "../src/events.js"; + +export const DEFAULT_MARGINS = "16px"; +export const SEENTHIS_EVENTS = [ + "@seenthis_storylines/ready", + "@seenthis_enabled", + "@seenthis_disabled", + "@seenthis_metric", + "@seenthis_detach", + "@seenthis_modal/opened", + "@seenthis_modal/closed", + "@seenthis_modal/beforeopen", + "@seenthis_modal/beforeclose", +]; + +const classNames: Record = { + container: "storylines-container", + expandedBody: "seenthis-storylines-fullscreen", +}; +const containerElements: Record = {}; +const frameElements: Record = {}; +const isInitialized: Record = {}; + +export function calculateMargins(element: HTMLElement) { + const boundingClientRect = getBoundingClientRect(element); + const wrapperLeftMargin = window.getComputedStyle(element).marginLeft; + const marginLeft = boundingClientRect.left - parseInt(wrapperLeftMargin, 10); + + if (boundingClientRect.width === 0 || marginLeft === 0) { + element.style.setProperty("--storylines-margins", DEFAULT_MARGINS); + element.style.setProperty("--storylines-margin-left", DEFAULT_MARGINS); + return; + } + element.style.setProperty("--storylines-margin-left", `-${marginLeft}px`); + element.style.setProperty("--storylines-margins", `${marginLeft * 2}px`); +} + +export function getFrameByEvent(event: MessageEvent) { + const isAncestor = (childWindow: Window, frameWindow: Window) => { + if (frameWindow === childWindow) { + return true; + } else if (childWindow === window.top) { + return false; + } + if (!childWindow?.parent) return false; + return isAncestor(childWindow.parent, frameWindow); + }; + const iframeThatMatchesSource = Array.from( + document.getElementsByTagName("iframe") + ).find((frame) => + isAncestor(event.source as Window, frame.contentWindow as Window) + ); + return iframeThatMatchesSource || null; +} + +export function addStyleToSingleChildAncestors( + element: HTMLElement, + { key, value }: { key: string; value: string } +) { + if (!element || !key) return; + if ( + key in element.style && + "offsetWidth" in element && + element.offsetWidth < getWinDimensions().innerWidth + ) { + element.style.setProperty(key, value); + } + if (!element.parentElement || element.parentElement?.children.length > 1) { + return; + } + addStyleToSingleChildAncestors(element.parentElement, { key, value }); +} + +export function findAdWrapper(target: HTMLDivElement) { + return target?.parentElement?.parentElement; +} + +export function applyFullWidth(target: HTMLDivElement) { + const adWrapper = findAdWrapper(target); + if (adWrapper) { + addStyleToSingleChildAncestors(adWrapper, { key: "width", value: "100%" }); + } +} + +export function applyAutoHeight(target: HTMLDivElement) { + const adWrapper = findAdWrapper(target); + if (adWrapper) { + addStyleToSingleChildAncestors(adWrapper, { key: "height", value: "auto" }); + addStyleToSingleChildAncestors(adWrapper, { + key: "min-height", + value: "auto", + }); + } +} + +// listen to messages from iframes +window.addEventListener("message", (event) => { + if (!["https://video.seenthis.se"].includes(event?.origin)) return; + + const data = event?.data; + if (!data) return; + + switch (data.type) { + case "storylines:init": { + const storyKey = data.storyKey; + if (!storyKey || isInitialized[storyKey]) return; + isInitialized[storyKey] = true; + + frameElements[storyKey] = getFrameByEvent(event); + containerElements[storyKey] = frameElements[storyKey] + ?.parentElement as HTMLDivElement; + event.source?.postMessage( + "storylines:init-ok", + "*" as WindowPostMessageOptions + ); + + const styleEl = document.createElement("style"); + styleEl.textContent = data.css; + document.head.appendChild(styleEl); + if (data.fixes.includes("full-width")) { + applyFullWidth(containerElements[storyKey]); + } + if (data.fixes.includes("auto-height")) { + applyAutoHeight(containerElements[storyKey]); + } + + containerElements[storyKey]?.classList.add(classNames.container); + calculateMargins(containerElements[storyKey]); + + events.emit(EVENTS.BILLABLE_EVENT, { + vendor: "seenthis", + type: "storylines_init", + }); + break; + } + case "@seenthis_modal/beforeopen": { + const storyKey = data.detail.storyKey; + document.body.classList.add(classNames.expandedBody); + containerElements[storyKey]?.classList.add("expanded"); + break; + } + case "@seenthis_modal/beforeclose": { + const storyKey = data.detail.storyKey; + document.body.classList.remove(classNames.expandedBody); + containerElements[storyKey]?.classList.remove("expanded"); + break; + } + } + + // dispatch SEENTHIS_EVENTS to parent window + if (SEENTHIS_EVENTS.includes(data.type)) { + window.dispatchEvent(new CustomEvent(data.type, { detail: data })); + } +}); + +Array.from(window.frames).forEach((frame) => { + frame.postMessage("storylines:bridge-ready", "*"); +}); diff --git a/modules/semantiqRtdProvider.js b/modules/semantiqRtdProvider.js index 3ee6d986cce..0308735aaa8 100644 --- a/modules/semantiqRtdProvider.js +++ b/modules/semantiqRtdProvider.js @@ -160,7 +160,7 @@ const dispatchPageImpressionEvent = (companyId) => { event_type: 'pageImpression', page_impression_id: pageImpressionId, source: 'semantiqPrebidModule', - url: pageUrl, + page_url: pageUrl, }; return fetch(EVENT_COLLECTOR_URL, { diff --git a/modules/sevioBidAdapter.js b/modules/sevioBidAdapter.js index 103c2c2c7f7..0b34c3098dd 100644 --- a/modules/sevioBidAdapter.js +++ b/modules/sevioBidAdapter.js @@ -3,7 +3,14 @@ import { detectWalletsPresence} from "../libraries/cryptoUtils/wallets.js"; import { registerBidder } from "../src/adapters/bidderFactory.js"; import { BANNER, NATIVE } from "../src/mediaTypes.js"; import { config } from "../src/config.js"; +import {getDomComplexity, getPageDescription, getPageTitle} from "../libraries/fpdUtils/pageInfo.js"; +import * as converter from '../libraries/ortbConverter/converter.js'; +const PREBID_VERSION = '$prebid.version$'; +const ADAPTER_VERSION = '1.0'; +const ORTB = converter.ortbConverter({ + context: { ttl: 300 } +}); const BIDDER_CODE = "sevio"; const GVLID = `1393`; const ENDPOINT_URL = "https://req.adx.ws/prebid"; @@ -14,6 +21,10 @@ const detectAdType = (bid) => ["native", "banner"].find((t) => bid.mediaTypes?.[t]) || "unknown" ).toUpperCase(); +const getReferrerInfo = (bidderRequest) => { + return bidderRequest?.refererInfo?.page ?? ''; +} + const parseNativeAd = function (bid) { try { const nativeAd = JSON.parse(bid.ad); @@ -26,18 +37,18 @@ const parseNativeAd = function (bid) { if (asset.data) { const value = asset.data.value; switch (asset.data.type) { - case 1: if (value) native.sponsoredBy = value; break; - case 2: if (value) native.body = value; break; + case 1: if (value) native.sponsored = value; break; + case 2: if (value) native.desc = value; break; case 3: if (value) native.rating = value; break; case 4: if (value) native.likes = value; break; case 5: if (value) native.downloads = value; break; case 6: if (value) native.price = value; break; - case 7: if (value) native.salePrice = value; break; + case 7: if (value) native.saleprice = value; break; case 8: if (value) native.phone = value; break; case 9: if (value) native.address = value; break; - case 10: if (value) native.body2 = value; break; - case 11: if (value) native.displayUrl = value; break; - case 12: if (value) native.cta = value; break; + case 10: if (value) native.desc2 = value; break; + case 11: if (value) native.displayurl = value; break; + case 12: if (value) native.ctatext = value; break; default: break; } } @@ -120,21 +131,67 @@ export const spec = { buildRequests: function (bidRequests, bidderRequest) { const userSyncEnabled = config.getConfig("userSync.syncEnabled"); + const currencyConfig = config.getConfig('currency'); + const currency = + currencyConfig?.adServerCurrency || + currencyConfig?.defaultCurrency || + null; + // (!) that avoids top-level side effects (the thing that can stop registerBidder from running) + const computeTTFB = (w = (typeof window !== 'undefined' ? window : undefined)) => { + try { + const wt = (() => { try { return w?.top ?? w; } catch { return w; } })(); + const p = wt?.performance || wt?.webkitPerformance || wt?.msPerformance || wt?.mozPerformance; + if (!p) return ''; + + if (typeof p.getEntriesByType === 'function') { + const nav = p.getEntriesByType('navigation')?.[0]; + if (nav?.responseStart > 0 && nav?.requestStart > 0) { + return String(Math.round(nav.responseStart - nav.requestStart)); + } + } + + const t = p.timing; + if (t?.responseStart > 0 && t?.requestStart > 0) { + return String(t.responseStart - t.requestStart); + } + + return ''; + } catch { + return ''; + } + }; + + // simple caching + const getTTFBOnce = (() => { + let cached = false; + let done = false; + return () => { + if (done) return cached; + done = true; + cached = computeTTFB(); + return cached; + }; + })(); + const ortbRequest = ORTB.toORTB({ bidderRequest, bidRequests }); if (bidRequests.length === 0) { return []; } - const gdpr = bidderRequest.gdprConsent; - const usp = bidderRequest.uspConsent; - const gpp = bidderRequest.gppConsent; + const gdpr = bidderRequest?.gdprConsent; + const usp = bidderRequest?.uspConsent; + const gpp = bidderRequest?.gppConsent; const hasWallet = detectWalletsPresence(); return bidRequests.map((bidRequest) => { const isNative = detectAdType(bidRequest)?.toLowerCase() === 'native'; - const size = bidRequest.mediaTypes?.banner?.sizes[0] || bidRequest.mediaTypes?.native?.sizes[0] || []; - const width = size[0]; - const height = size[1]; + const adSizes = bidRequest.mediaTypes?.banner?.sizes || bidRequest.mediaTypes?.native?.sizes || []; + const formattedSizes = Array.isArray(adSizes) + ? adSizes + .filter(size => Array.isArray(size) && size.length === 2) + .map(([width, height]) => ({ width, height })) + : []; const originalAssets = bidRequest.mediaTypes?.native?.ortb?.assets || []; + // convert icon to img type 1 const processedAssets = originalAssets.map(asset => { if (asset.icon) { @@ -150,6 +207,7 @@ export const spec = { } return asset; }); + const payload = { userLanguage: navigator.language, pageUrl: bidRequest?.refererInfo?.page, @@ -159,19 +217,17 @@ export const spec = { source: eid.source, id: eid.uids?.[0]?.id })).filter(eid => eid.source && eid.id), + ...(currency ? { currency } : {}), ads: [ { - maxSize: { - width: width, - height: height, - }, + sizes: formattedSizes, referenceId: bidRequest.params.referenceId, tagId: bidRequest.params.zone, type: detectAdType(bidRequest), ...(isNative && { nativeRequest: { ver: "1.2", assets: processedAssets || {}} }) }, ], - keywords: { tokens: bidRequest.params?.keywords || [] }, + keywords: { tokens: ortbRequest?.site?.keywords || bidRequest.params?.keywords || [] }, privacy: { gpp: gpp?.consentString || "", tcfeu: gdpr?.consentString || "", @@ -181,11 +237,34 @@ export const spec = { wdb: hasWallet, externalRef: bidRequest.bidId, userSyncOption: userSyncEnabled === false ? "OFF" : "BIDDERS", + referer: getReferrerInfo(bidderRequest), + pageReferer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), + domComplexity: getDomComplexity(document), + device: bidderRequest?.ortb2?.device || {}, + deviceWidth: screen.width, + deviceHeight: screen.height, + timeout: bidderRequest?.timeout, + viewportHeight: utils.getWinDimensions().visualViewport.height, + viewportWidth: utils.getWinDimensions().visualViewport.width, + timeToFirstByte: getTTFBOnce(), + ext: { + ...(bidderRequest?.ortb2?.ext || {}), + adapter_version: ADAPTER_VERSION, + prebid_version: PREBID_VERSION + } }; + const wrapperOn = + typeof window !== "undefined" && window.sevio_wrapper === true; + + const url = wrapperOn + ? `${ENDPOINT_URL}?wrapper=true` + : ENDPOINT_URL; return { method: ACTION_METHOD, - url: ENDPOINT_URL, + url, data: payload, bidRequest: bidRequests[0], }; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 4830c799793..4d84c21a99e 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,11 +1,11 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { getDNT } from '../libraries/dnt/index.js'; import { handleCookieSync, PID_STORAGE_NAME, prepareSplitImps } from '../libraries/equativUtils/equativUtils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { deepAccess, generateUUID, inIframe, isPlainObject, logWarn, mergeDeep } from '../src/utils.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess, generateUUID, inIframe, isPlainObject, logWarn, mergeDeep } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -45,8 +45,8 @@ export const sharethroughInternal = { export const converter = ortbConverter({ context: { netRevenue: true, - ttl: 360 - } + ttl: 360, + }, }); export const sharethroughAdapterSpec = { @@ -99,7 +99,7 @@ export const sharethroughAdapterSpec = { test: 0, }; - req.user = firstPartyData.user ?? {} + req.user = firstPartyData.user ?? {}; if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -108,7 +108,7 @@ export const sharethroughAdapterSpec = { eqtvNetworkId = bidRequests[0].params.equativNetworkId; req.site.publisher = { id: bidRequests[0].params.equativNetworkId, - ...req.site.publisher + ...req.site.publisher, }; const pid = storage.getDataFromLocalStorage(PID_STORAGE_NAME); if (pid) { @@ -190,7 +190,9 @@ export const sharethroughAdapterSpec = { if (propIsTypeArray) { const notAssignable = (!Array.isArray(vidReq[prop]) || vidReq[prop].length === 0) && vidReq[prop]; if (notAssignable) { - logWarn(`${IDENTIFIER_PREFIX} Invalid video request property: "${prop}" must be an array with at least 1 entry. Value supplied: "${vidReq[prop]}". This will not be added to the bid request.`); + logWarn( + `${IDENTIFIER_PREFIX} Invalid video request property: "${prop}" must be an array with at least 1 entry. Value supplied: "${vidReq[prop]}". This will not be added to the bid request.` + ); return; } } @@ -207,24 +209,39 @@ export const sharethroughAdapterSpec = { }; const propertiesToConsider = [ - 'api', 'battr', 'companiontype', 'delivery', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'plcmt', 'protocols', 'skip', 'skipafter', 'skipmin', 'startdelay' + 'api', + 'battr', + 'companiontype', + 'delivery', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'plcmt', + 'protocols', + 'skip', + 'skipafter', + 'skipmin', + 'startdelay', ]; if (!isEqtvTest) { propertiesToConsider.push('companionad'); } - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { applyVideoProperty(propertyToConsider, videoRequest, impression); }); } else if (isEqtvTest && nativeRequest) { const nativeImp = converter.toORTB({ bidRequests: [bidReq], - bidderRequest + bidderRequest, }); impression.native = { - ...nativeImp.imp[0].native + ...nativeImp.imp[0].native, }; } else { impression.banner = { @@ -232,7 +249,8 @@ export const sharethroughAdapterSpec = { topframe: inIframe() ? 0 : 1, format: bidReq.sizes.map((size) => ({ w: +size[0], h: +size[1] })), }; - const battr = deepAccess(bidReq, 'mediaTypes.banner.battr', null) || deepAccess(bidReq, 'ortb2Imp.banner.battr'); + const battr = + deepAccess(bidReq, 'mediaTypes.banner.battr', null) || deepAccess(bidReq, 'ortb2Imp.banner.battr'); if (battr) impression.banner.battr = battr; } @@ -248,7 +266,7 @@ export const sharethroughAdapterSpec = { }) .filter((imp) => !!imp); - let splitImps = [] + let splitImps = []; if (isEqtvTest) { const bid = bidRequests[0]; const currency = config.getConfig('currency.adServerCurrency') || 'USD'; @@ -309,8 +327,8 @@ export const sharethroughAdapterSpec = { brandName: bid.ext?.brandName || null, demandSource: bid.ext?.demandSource || null, dchain: bid.ext?.dchain || null, - primaryCatId: bid.ext?.primaryCatId || null, - secondaryCatIds: bid.ext?.secondaryCatIds || null, + primaryCatId: bid.ext?.primaryCatId || '', + secondaryCatIds: bid.ext?.secondaryCatIds || [], mediaType: bid.ext?.mediaType || null, }, }; @@ -320,7 +338,7 @@ export const sharethroughAdapterSpec = { response.vastXml = bid.adm; } else if (response.mediaType === NATIVE) { response.native = { - ortb: JSON.parse(bid.adm) + ortb: JSON.parse(bid.adm), }; } @@ -339,7 +357,7 @@ export const sharethroughAdapterSpec = { getUserSyncs: (syncOptions, serverResponses, gdprConsent) => { if (isEqtvTest) { - return handleCookieSync(syncOptions, serverResponses, gdprConsent, eqtvNetworkId, storage) + return handleCookieSync(syncOptions, serverResponses, gdprConsent, eqtvNetworkId, storage); } else { const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; @@ -349,13 +367,13 @@ export const sharethroughAdapterSpec = { }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => { }, + onTimeout: (data) => {}, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => { }, + onBidWon: (bid) => {}, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => { }, + onSetTargeting: (bid) => {}, }; function getBidRequestFloor(bid) { diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 32dcc075172..03199e99d52 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {deepAccess, deepSetValue, isEmpty, isNumber, logError, logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index 853c0d1a29a..9565f318ead 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -25,6 +25,8 @@ const ALIASES = { 'addigi': {area: '1', pid: '425'}, 'jambojar': {area: '1', pid: '426'}, 'anzu': {area: '1', pid: '445'}, + 'amcom': {area: '1', pid: '397'}, + 'adastra': {area: '1', pid: '33'}, }; const BASE_URLS = { @@ -40,6 +42,8 @@ const BASE_URLS = { 'jambojar': 'https://jambojar-prebid.attekmi.co/pbjs', 'jambojar-apac': 'https://jambojar-apac-prebid.attekmi.co/pbjs', 'anzu': 'https://anzu-prebid.attekmi.co/pbjs', + 'amcom': 'https://amcom-prebid.attekmi.co/pbjs', + 'adastra': 'https://adastra-prebid.attekmi.co/pbjs', }; const adapterState = {}; diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index 4a0938d149d..00f8616a665 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { logError, deepAccess, diff --git a/modules/smartytechBidAdapter.js b/modules/smartytechBidAdapter.js index 6f3de10be3c..373faebf5a4 100644 --- a/modules/smartytechBidAdapter.js +++ b/modules/smartytechBidAdapter.js @@ -1,12 +1,51 @@ -import {buildUrl, deepAccess} from '../src/utils.js' +import {buildUrl, deepAccess, isArray, generateUUID} from '../src/utils.js' import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {chunk} from '../libraries/chunk/chunk.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {findRootDomain} from '../src/fpd/rootDomain.js'; const BIDDER_CODE = 'smartytech'; export const ENDPOINT_PROTOCOL = 'https'; export const ENDPOINT_DOMAIN = 'server.smartytech.io'; export const ENDPOINT_PATH = '/hb/v2/bidder'; +// Alias User ID constants +const AUID_COOKIE_NAME = '_smartytech_auid'; +const AUID_COOKIE_EXPIRATION_DAYS = 1825; // 5 years + +// Storage manager for cookies +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +/** + * Get or generate Alias User ID (auId) + * - Checks if auId exists in cookie + * - If not, generates new UUID and stores it in cookie on root domain + * @returns {string|null} The alias user ID or null if cookies are not enabled + */ +export function getAliasUserId() { + if (!storage.cookiesAreEnabled()) { + return null; + } + + let auId = storage.getCookie(AUID_COOKIE_NAME); + + if (auId && auId.length > 0) { + return auId; + } + + auId = generateUUID(); + + const expirationDate = new Date(); + expirationDate.setTime(expirationDate.getTime() + (AUID_COOKIE_EXPIRATION_DAYS * 24 * 60 * 60 * 1000)); + const expires = expirationDate.toUTCString(); + + storage.setCookie(AUID_COOKIE_NAME, auId, expires, 'Lax', findRootDomain()); + + return auId; +} + export const spec = { supportedMediaTypes: [ BANNER, VIDEO ], code: BIDDER_CODE, @@ -54,6 +93,8 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const referer = bidderRequest?.refererInfo?.page || window.location.href; + const auId = getAliasUserId(); + const bidRequests = validBidRequests.map((validBidRequest) => { const video = deepAccess(validBidRequest, 'mediaTypes.video', false); const banner = deepAccess(validBidRequest, 'mediaTypes.banner', false); @@ -66,6 +107,10 @@ export const spec = { bidId: validBidRequest.bidId }; + if (auId) { + oneRequest.auId = auId; + } + if (video) { oneRequest.video = video; @@ -80,20 +125,60 @@ export const spec = { } } + // Add user IDs if available + const userIds = deepAccess(validBidRequest, 'userIdAsEids'); + if (userIds && isArray(userIds) && userIds.length > 0) { + oneRequest.userIds = userIds; + } + + // Add GDPR consent if available + if (bidderRequest && bidderRequest.gdprConsent) { + oneRequest.gdprConsent = { + consentString: bidderRequest.gdprConsent.consentString || '' + }; + + if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + oneRequest.gdprConsent.gdprApplies = bidderRequest.gdprConsent.gdprApplies; + } + + if (bidderRequest.gdprConsent.addtlConsent) { + oneRequest.gdprConsent.addtlConsent = bidderRequest.gdprConsent.addtlConsent; + } + } + + // Add CCPA/USP consent if available + if (bidderRequest && bidderRequest.uspConsent) { + oneRequest.uspConsent = bidderRequest.uspConsent; + } + + // Add COPPA flag if configured + const coppa = config.getConfig('coppa'); + if (coppa) { + oneRequest.coppa = coppa; + } + return oneRequest }); - const adPartnerRequestUrl = buildUrl({ + const smartytechRequestUrl = buildUrl({ protocol: ENDPOINT_PROTOCOL, hostname: ENDPOINT_DOMAIN, pathname: ENDPOINT_PATH, }); - return { + // Get chunk size from adapter configuration + const adapterSettings = config.getConfig(BIDDER_CODE) || {}; + const chunkSize = deepAccess(adapterSettings, 'chunkSize', 10); + + // Split bid requests into chunks + const bidChunks = chunk(bidRequests, chunkSize); + + // Return array of request objects, one for each chunk + return bidChunks.map(bidChunk => ({ method: 'POST', - url: adPartnerRequestUrl, - data: bidRequests - }; + url: smartytechRequestUrl, + data: bidChunk + })); }, interpretResponse: function (serverResponse, bidRequest) { diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index 2addd1ae3f4..29534ae7318 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; @@ -18,7 +18,6 @@ const getConfig = config.getConfig; const storageManager = getStorageManager({bidderCode: BIDDER_CODE}); const refreshes = {}; const placementCounters = {}; -const pageViewId = generateUUID(); const pageViewStart = new Date().getTime(); let auctionCounter = 0; @@ -43,7 +42,7 @@ export const spec = { site: deepAccess(bidRequests, '0.params.site'), sessionId: getSessionId(), counter: auctionCounter++, - pageViewId: pageViewId, + pageViewId: bidderRequest.pageViewId, pageViewStart: pageViewStart, gdprConsent: gdprApplies === true ? hasFullGdprConsent(deepAccess(bidderRequest, 'gdprConsent')) : false, cur: getCurrencies(), diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js index b84b662990c..13cb0198594 100644 --- a/modules/sparteoBidAdapter.js +++ b/modules/sparteoBidAdapter.js @@ -12,7 +12,7 @@ const BIDDER_CODE = 'sparteo'; const GVLID = 1028; const TTL = 60; const HTTP_METHOD = 'POST'; -const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const REQUEST_URL = `https://bid.sparteo.com/auction?network_id=\${NETWORK_ID}\${SITE_DOMAIN_QUERY}\${APP_DOMAIN_QUERY}\${BUNDLE_QUERY}`; const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; let isSynced = window.sparteoCrossfire?.started || false; @@ -25,14 +25,25 @@ const converter = ortbConverter({ request(buildRequest, imps, bidderRequest, context) { const request = buildRequest(imps, bidderRequest, context); - deepSetValue(request, 'site.publisher.ext.params.pbjsVersion', '$prebid.version$'); - - if (bidderRequest.bids[0].params.networkId) { - request.site.publisher.ext.params.networkId = bidderRequest.bids[0].params.networkId; + if (!!(bidderRequest?.ortb2?.site) && !!(bidderRequest?.ortb2?.app)) { + request.site = bidderRequest.ortb2.site; + delete request.app; } - if (bidderRequest.bids[0].params.publisherId) { - request.site.publisher.ext.params.publisherId = bidderRequest.bids[0].params.publisherId; + const hasSite = !!request.site; + const hasApp = !!request.app; + const root = hasSite ? 'site' : (hasApp ? 'app' : null); + + if (root) { + deepSetValue(request, `${root}.publisher.ext.params.pbjsVersion`, '$prebid.version$'); + const networkId = bidderRequest?.bids?.[0]?.params?.networkId; + if (networkId) { + deepSetValue(request, `${root}.publisher.ext.params.networkId`, networkId); + } + const pubId = bidderRequest?.bids?.[0]?.params?.publisherId; + if (pubId) { + deepSetValue(request, `${root}.publisher.ext.params.publisherId`, pubId); + } } return request; @@ -105,6 +116,57 @@ function outstreamRender(bid) { }); } +function replaceMacros(payload, endpoint) { + const networkId = + payload?.site?.publisher?.ext?.params?.networkId ?? + payload?.app?.publisher?.ext?.params?.networkId; + + let siteDomain; + let appDomain; + let bundle; + + if (payload?.site) { + siteDomain = payload.site?.domain; + if (!siteDomain && payload.site?.page) { + try { siteDomain = new URL(payload.site.page).hostname; } catch (e) { } + } + if (siteDomain) { + siteDomain = siteDomain.trim().split('/')[0].split(':')[0].replace(/^www\./, ''); + } else { + logWarn('Domain not found. Missing the site.domain or the site.page field'); + siteDomain = 'unknown'; + } + } else if (payload?.app) { + appDomain = payload.app?.domain || ''; + if (appDomain) { + appDomain = appDomain.trim().split('/')[0].split(':')[0].replace(/^www\./, ''); + } else { + appDomain = 'unknown'; + } + + const raw = payload.app?.bundle ?? ''; + const trimmed = String(raw).trim(); + if (!trimmed || trimmed.toLowerCase() === 'null') { + logWarn('Bundle not found. Missing the app.bundle field.'); + bundle = 'unknown'; + } else { + bundle = trimmed; + } + } + + const macroMap = { + NETWORK_ID: networkId ?? '', + BUNDLE_QUERY: payload?.app ? (bundle ? `&bundle=${encodeURIComponent(bundle)}` : '') : '', + SITE_DOMAIN_QUERY: siteDomain ? `&site_domain=${encodeURIComponent(siteDomain)}` : '', + APP_DOMAIN_QUERY: appDomain ? `&app_domain=${encodeURIComponent(appDomain)}` : '' + }; + + return endpoint.replace( + /\$\{(NETWORK_ID|SITE_DOMAIN_QUERY|APP_DOMAIN_QUERY|BUNDLE_QUERY)\}/g, + (_, key) => String(macroMap[key] ?? '') + ); +} + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -166,9 +228,12 @@ export const spec = { buildRequests: function (bidRequests, bidderRequest) { const payload = converter.toORTB({bidRequests, bidderRequest}) + const endpoint = bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL; + const url = replaceMacros(payload, endpoint); + return { method: HTTP_METHOD, - url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + url: url, data: payload }; }, diff --git a/modules/ssp_genieeBidAdapter.js b/modules/ssp_genieeBidAdapter.js index c768c511e9c..c4464f0f59a 100644 --- a/modules/ssp_genieeBidAdapter.js +++ b/modules/ssp_genieeBidAdapter.js @@ -342,11 +342,22 @@ export const spec = { */ isBidRequestValid: function (bidRequest) { if (!bidRequest.params.zoneId) return false; - const currencyType = config.getConfig('currency.adServerCurrency'); - if (typeof currencyType === 'string' && ALLOWED_CURRENCIES.indexOf(currencyType) === -1) { - utils.logError('Invalid currency type, we support only JPY and USD!'); - return false; + + if (bidRequest.params.hasOwnProperty('currency')) { + const bidCurrency = bidRequest.params.currency; + + if (!ALLOWED_CURRENCIES.includes(bidCurrency)) { + utils.logError(`[${BIDDER_CODE}] Currency "${bidCurrency}" in bid params is not supported. Supported are: ${ALLOWED_CURRENCIES.join(', ')}.`); + return false; + } + } else { + const adServerCurrency = config.getConfig('currency.adServerCurrency'); + if (typeof adServerCurrency === 'string' && !ALLOWED_CURRENCIES.includes(adServerCurrency)) { + utils.logError(`[${BIDDER_CODE}] adServerCurrency "${adServerCurrency}" is not supported. Supported are: ${ALLOWED_CURRENCIES.join(', ')}.`); + return false; + } } + return true; }, /** diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js index 76a68f8ce95..74629f2cc9c 100644 --- a/modules/startioBidAdapter.js +++ b/modules/startioBidAdapter.js @@ -7,7 +7,7 @@ import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; const BIDDER_CODE = 'startio'; const METHOD = 'POST'; const GVLID = 1216; -const ENDPOINT_URL = `http://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; +const ENDPOINT_URL = `https://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; const converter = ortbConverter({ imp(buildImp, bidRequest, context) { diff --git a/modules/suimBidAdapter.js b/modules/suimBidAdapter.js index 0e4374a83f5..57ecbcaac1e 100644 --- a/modules/suimBidAdapter.js +++ b/modules/suimBidAdapter.js @@ -14,8 +14,8 @@ import { getBidIdParameter, isEmpty } from '../src/utils.js'; */ const BIDDER_CODE = 'suim'; -const ENDPOINT = 'https://bid.suimad.com/api/v1/prebids'; -const SYNC_URL = 'https://bid.suimad.com/api/v1/logs/usersync'; +const ENDPOINT = 'https://ad.suimad.com/bid'; +const SYNC_URL = 'https://ad.suimad.com/usersync'; export const spec = { code: BIDDER_CODE, @@ -60,7 +60,6 @@ export const spec = { data: data, options: { contentType: 'text/plain', - withCredentials: false, }, }; }); diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 83df42c467a..2db4076f57f 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -210,7 +210,7 @@ export const spec = { }, interpretResponse: function (response, serverRequest) { - if (isEmpty(response.body)) { + if (isEmpty(response.body.seatbid)) { return []; } deepSetValue(response, 'body.seatbid.0.bid.0.impid', deepAccess(serverRequest, 'data.imp.0.id')) diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 916cb261146..6ca3adb4d54 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { logWarn, deepAccess, isFn, isPlainObject, isBoolean, isNumber, isStr, isArray } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; @@ -16,7 +16,7 @@ const BIDDER_CODE = 'tappx'; const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.4'; +const TAPPX_BIDDER_VERSION = '0.1.5'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -209,6 +209,7 @@ function interpretBid(serverBid, request) { if (typeof serverBid.lurl !== 'undefined') { bidReturned.lurl = serverBid.lurl } if (typeof serverBid.nurl !== 'undefined') { bidReturned.nurl = serverBid.nurl } if (typeof serverBid.burl !== 'undefined') { bidReturned.burl = serverBid.burl } + if (typeof serverBid.adomain !== 'undefined') { bidReturned.adomain = serverBid.adomain } if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') { bidReturned.vastXml = serverBid.adm; @@ -231,7 +232,7 @@ function interpretBid(serverBid, request) { } if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) { - bidReturned.meta = { advertiserDomains: request.bids?.adomain }; + bidReturned.meta = { advertiserDomains: bidReturned.adomain }; } return bidReturned; diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 90204387bb5..15ac4376548 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { logInfo, isEmpty, deepAccess, parseUrl, parseSizesInput, _map } from '../src/utils.js'; import { BANNER, diff --git a/modules/toponBidAdapter.js b/modules/toponBidAdapter.js new file mode 100644 index 00000000000..263069b7f34 --- /dev/null +++ b/modules/toponBidAdapter.js @@ -0,0 +1,170 @@ +import { logWarn, generateUUID } from "../src/utils.js"; +import { registerBidder } from "../src/adapters/bidderFactory.js"; +import { BANNER } from "../src/mediaTypes.js"; +import { ortbConverter } from "../libraries/ortbConverter/converter.js"; + +const PREBID_VERSION = "$prebid.version$"; +const BIDDER_CODE = "topon"; +const LOG_PREFIX = "TopOn"; +const GVLID = 1305; +const ENDPOINT = "https://web-rtb.anyrtb.com/ortb/prebid"; +const DEFAULT_TTL = 360; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_TTL, + currency: "USD", + }, + imp(buildImp, bidRequest, context) { + const mediaType = + bidRequest.mediaType || Object.keys(bidRequest.mediaTypes || {})[0]; + + if (mediaType === "banner") { + const sizes = bidRequest.mediaTypes.banner.sizes; + return { + id: bidRequest.bidId, + banner: { + format: sizes.map(([w, h]) => ({ w, h })), + }, + tagid: bidRequest.adUnitCode, + }; + } + + return null; + }, + request(buildRequest, imps, bidderRequest, context) { + const requestId = + bidderRequest.bidderRequestId || + bidderRequest.auctionId || + generateUUID(); + const ortb2 = bidderRequest.ortb2 || {}; + + return { + id: requestId, + imp: imps, + site: { + page: ortb2.site?.page || bidderRequest.refererInfo?.page, + domain: ortb2.site?.domain || location.hostname, + }, + device: ortb2.device || {}, + ext: { + prebid: { + channel: { + version: PREBID_VERSION, + source: "pbjs", + }, + }, + }, + source: { + ext: { + prebid: 1, + }, + }, + }; + }, + bidResponse(buildBidResponse, bid, context) { + return buildBidResponse(bid, context); + }, + response(buildResponse, bidResponses, ortbResponse, context) { + return buildResponse(bidResponses, ortbResponse, context); + }, + overrides: { + imp: { + bidfloor: false, + extBidfloor: false, + }, + bidResponse: { + native: false, + }, + }, +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + isBidRequestValid: (bid) => { + if (!(bid && bid.params)) { + return false; + } + const { pubid } = bid.params || {}; + if (!pubid) { + return false; + } else if (typeof pubid !== "string") { + return false; + } + return true; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const { pubid } = bidderRequest?.bids?.[0]?.params || {}; + const ortbRequest = converter.toORTB({ validBidRequests, bidderRequest }); + + const url = ENDPOINT + "?pubid=" + pubid; + return { + method: "POST", + url, + data: ortbRequest, + }; + }, + interpretResponse: (response, request) => { + if (!response.body || typeof response.body !== "object") { + return; + } + + const { id, seatbid: seatbids } = response.body; + if (id && seatbids) { + seatbids.forEach((seatbid) => { + seatbid.bid.forEach((bid) => { + let height = bid.h; + let width = bid.w; + const isBanner = bid.mtype === 1; + if ( + (!height || !width) && + request.data && + request.data.imp && + request.data.imp.length > 0 + ) { + request.data.imp.forEach((req) => { + if (bid.impid === req.id) { + if (isBanner) { + let bannerHeight = 1; + let bannerWidth = 1; + if (req.banner.format && req.banner.format.length > 0) { + bannerHeight = req.banner.format[0].h; + bannerWidth = req.banner.format[0].w; + } + height = bannerHeight; + width = bannerWidth; + } else { + height = 1; + width = 1; + } + } + }); + bid.w = width; + bid.h = height; + } + }); + }); + } + + const { bids } = converter.fromORTB({ + response: response.body, + request: request.data, + }); + + return bids; + }, + getUserSyncs: ( + syncOptions, + responses, + gdprConsent, + uspConsent, + gppConsent + ) => {}, + onBidWon: (bid) => { + logWarn(`[${LOG_PREFIX}] Bid won: ${JSON.stringify(bid)}`); + }, +}; +registerBidder(spec); diff --git a/modules/toponBidAdapter.md b/modules/toponBidAdapter.md new file mode 100644 index 00000000000..e42ad438fb8 --- /dev/null +++ b/modules/toponBidAdapter.md @@ -0,0 +1,29 @@ +# Overview + +``` +Module Name: TopOn Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@toponad.net +``` + +# Description + +TopOn Bid Adapter for Prebid.js + +# Sample Banner Ad Unit: For Publishers + +``` +var adUnits = [{ + code: 'test-div', + sizes: [ + [300, 250], + [728, 90] + ], + bids: [{ + bidder: 'topon', + params: { + pubid: 'pub-uuid', // required, must be a string, not an integer or other js type. + } + }] +}]; +``` diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 8054e723e38..f73bd470b14 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { generateUUID, _each, deepAccess } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; diff --git a/modules/uniquestWidgetBidAdapter.js b/modules/uniquest_widgetBidAdapter.js similarity index 100% rename from modules/uniquestWidgetBidAdapter.js rename to modules/uniquest_widgetBidAdapter.js diff --git a/modules/uniquestWidgetBidAdapter.md b/modules/uniquest_widgetBidAdapter.md similarity index 100% rename from modules/uniquestWidgetBidAdapter.md rename to modules/uniquest_widgetBidAdapter.md diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index a8fe20c0eca..801c68acc2d 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -311,6 +311,10 @@ function buildImpObject(bid) { impObject.ext.bidder.adslotExists = _isAdSlotExists(adUnitCode); } + if (bid.ortb2Imp?.ext?.gpid) { + impObject.ext.gpid = bid.ortb2Imp.ext.gpid; + } + if (impObject.ext.bidder.uid && (impObject.banner || impObject.video)) { return impObject; } diff --git a/modules/welectBidAdapter.js b/modules/welectBidAdapter.js index b271679fc4f..ea2600247c3 100644 --- a/modules/welectBidAdapter.js +++ b/modules/welectBidAdapter.js @@ -119,6 +119,7 @@ export const spec = { ttl: responseBody.bidResponse.ttl, ad: responseBody.bidResponse.ad, vastUrl: responseBody.bidResponse.vastUrl, + mediaType: responseBody.bidResponse.mediaType, meta: { advertiserDomains: responseBody.bidResponse.meta.advertiserDomains } diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js index c0f4550a0ce..901bbcf29de 100644 --- a/modules/yandexBidAdapter.js +++ b/modules/yandexBidAdapter.js @@ -2,7 +2,8 @@ import { getCurrencyFromBidderRequest } from '../libraries/ortb2Utils/currency.j import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { _each, _map, deepAccess, deepSetValue, formatQS, triggerPixel, logInfo } from '../src/utils.js'; +import { inIframe, _each, _map, deepAccess, deepSetValue, formatQS, triggerPixel, logInfo } from '../src/utils.js'; +import { getBoundingClientRect } from '../libraries/boundingClientRect/boundingClientRect.js'; import { ajax } from '../src/ajax.js'; import { config as pbjsConfig } from '../src/config.js'; import { isWebdriverEnabled } from '../libraries/webdriver/webdriver.js'; @@ -49,11 +50,13 @@ import { isWebdriverEnabled } from '../libraries/webdriver/webdriver.js'; * @typedef {BidRequest & AdditionalBidRequestFields} ExtendedBidRequest */ +const BIDDER_DOMAIN = 'yandex.com'; + const BIDDER_CODE = 'yandex'; -const BIDDER_URL = 'https://yandex.ru/ads/prebid'; -const EVENT_TRACKER_URL = 'https://yandex.ru/ads/trace'; +const BIDDER_URL = '/ads/prebid'; +const EVENT_TRACKER_URL = '/ads/trace'; // We send data in 1% of cases -const DEFAULT_SAMPLING_RATE = 0.1; +const DEFAULT_SAMPLING_RATE = 0.01; const EVENT_LOG_RANDOM_NUMBER = Math.random(); const DEFAULT_TTL = 180; const DEFAULT_CURRENCY = 'EUR'; @@ -69,7 +72,7 @@ const ORTB_MTYPES = { }; const SSP_ID = 10500; -const ADAPTER_VERSION = '2.7.0'; +const ADAPTER_VERSION = '2.9.0'; const TRACKER_METHODS = { img: 1, @@ -163,11 +166,14 @@ export const spec = { const { pageId, impId } = extractPlacementIds(params); + const domain = getBidderDomain(); + const queryParams = { 'imp-id': impId, 'target-ref': targetRef || ortb2?.site?.domain, 'adapter-version': ADAPTER_VERSION, 'ssp-id': SSP_ID, + domain, }; const gdprApplies = Boolean(deepAccess(bidderRequest, 'gdprConsent.gdprApplies')); @@ -177,6 +183,14 @@ export const spec = { queryParams['tcf-consent'] = consentString; } + const adUnitElement = document.getElementById(bidRequest.params.pubcontainerid || bidRequest.adUnitCode); + const windowContext = getContext(adUnitElement); + const isIframe = inIframe(); + const coords = isIframe ? getFramePosition() : { + x: adUnitElement && getBoundingClientRect(adUnitElement).x, + y: adUnitElement && getBoundingClientRect(adUnitElement).y, + }; + const imp = { id: impId, banner: mapBanner(bidRequest), @@ -184,6 +198,10 @@ export const spec = { video: mapVideo(bidRequest), displaymanager: 'Prebid.js', displaymanagerver: '$prebid.version$', + ext: { + isvisible: isVisible(adUnitElement), + coords, + } }; const bidfloor = getBidfloor(bidRequest); @@ -223,10 +241,18 @@ export const spec = { deepSetValue(data, 'user.ext.eids', eids); } + deepSetValue(data, 'ext.isiframe', isIframe); + + if (windowContext) { + deepSetValue(data, 'device.ext.scroll.top', windowContext.scrollY); + deepSetValue(data, 'device.ext.scroll.left', windowContext.scrollX); + } + const queryParamsString = formatQS(queryParams); + const request = { method: 'POST', - url: BIDDER_URL + `/${pageId}?${queryParamsString}`, + url: `https://${domain}${BIDDER_URL}/${pageId}?${queryParamsString}`, data, options: { withCredentials, @@ -266,10 +292,7 @@ export const spec = { }, onBidderError: function({ error, bidderRequest }) { eventLog('PREBID_BIDDER_ERROR_EVENT', { - error: { - message: error?.reason?.message, - stack: error?.reason?.stack, - }, + error, bidderRequest, }); }, @@ -615,8 +638,135 @@ function eventLog(name, resp) { data: resp, }; - ajax(EVENT_TRACKER_URL, undefined, JSON.stringify(data), { method: 'POST', withCredentials: true }); + const domain = getBidderDomain(); + + ajax(`https://${domain}${EVENT_TRACKER_URL}`, undefined, JSON.stringify(data), { method: 'POST', withCredentials: true }); + } +} + +function getBidderDomain() { + const bidderConfig = pbjsConfig.getConfig(); + return bidderConfig?.yandex?.domain ?? BIDDER_DOMAIN; +} + +/** + * Determines the appropriate window context for a given DOM element by checking + * its presence in the current window's DOM or the top-level window's DOM. + * + * This is useful for cross-window/frame DOM interactions where security restrictions + * might apply (e.g., same-origin policy). The function safely handles cases where + * cross-window access might throw errors. + * + * @param {Element|null|undefined} elem - The DOM element to check. Can be falsy. + * @returns {Window|undefined} Returns the appropriate window object where the element + * belongs (current window or top window). Returns undefined if the element is not found + * in either context or if access is denied due to cross-origin restrictions. + */ +function getContext(elem) { + try { + // Check if the element exists and is in the current window's DOM + if (elem) { + if (window.document.body.contains(elem)) { + return window; // Element is in current window + } else if (window.top.document.body.contains(elem)) { + return window.top; // Element exists in top window's DOM + } + return undefined; // Element not found in any accessible context} + } + } catch (e) { + // Handle cases where cross-origin access to top window's DOM is blocked + return undefined; + } +} + +/** + * Checks if an element is visible in the DOM + * @param {Element} elem - The element to check for visibility + * @returns {boolean} True if the element is visible, false otherwise + */ +function isVisible(elem) { + // Return false for non-existent elements + if (!elem) { + return false; + } + + // Get the rendering context for the element + const context = getContext(elem); + + // Return false if no context is available (element not in DOM) + if (!context) { + return false; + } + + let currentElement = elem; + let iterations = 0; + const MAX_ITERATIONS = 250; + + // Traverse up the DOM tree to check parent elements + while (currentElement && iterations < MAX_ITERATIONS) { + iterations++; + + try { + // Get computed styles for the current element + const computedStyle = context.getComputedStyle(currentElement); + + // Check for hiding styles: display: none or visibility: hidden + const isHidden = computedStyle.display === 'none' || + computedStyle.visibility === 'hidden'; + + if (isHidden) { + return false; // Element is hidden + } + } catch (error) { + // If we can't access styles, assume element is not visible + return false; + } + + // Move to the parent element for the next iteration + currentElement = currentElement.parentElement; } + + // If we've reached the root without finding hiding styles, element is visible + return true; +} + +/** + * Calculates the cumulative position of the current frame within nested iframes. + * This is useful when you need the absolute position of an element within nested iframes + * relative to the outermost main window. + * + * @returns {Array} [totalLeft, totalTop] - Cumulative left and top offsets in pixels + */ +function getFramePosition() { + let currentWindow = window; + let iterationCount = 0; + let totalLeft = 0; + let totalTop = 0; + const MAX_ITERATIONS = 100; + + do { + iterationCount++; + + try { + // After first iteration, move to parent window + if (iterationCount > 1) { + currentWindow = currentWindow.parent; + } + + // Get the frame element containing current window and its position + const frameElement = currentWindow.frameElement; + const rect = getBoundingClientRect(frameElement); + + // Accumulate frame element's position offsets + totalLeft += rect.left; + totalTop += rect.top; + } catch (error) { + // Continue processing if we can't access frame element (e.g., cross-origin restriction) + // Error is ignored as we can't recover frame position information in this case + } + } while (iterationCount < MAX_ITERATIONS && currentWindow.parent !== currentWindow.self); + + return { x: totalLeft, y: totalTop }; } registerBidder(spec); diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 74733f97032..f606194e132 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -1,4 +1,4 @@ -import { getDNT } from '../libraries/navigatorData/dnt.js'; +import {getDNT} from '../libraries/dnt/index.js'; import { deepAccess, deepSetValue, diff --git a/modules/yieldoneBidAdapter.js b/modules/yieldoneBidAdapter.js index 4a5e3481b81..c71dadd1573 100644 --- a/modules/yieldoneBidAdapter.js +++ b/modules/yieldoneBidAdapter.js @@ -4,6 +4,7 @@ import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getBrowser, getOS} from '../libraries/userAgentUtils/index.js'; import {browserTypes, osTypes} from '../libraries/userAgentUtils/userAgentTypes.enums.js'; +import {BOL_LIKE_USER_AGENTS} from '../libraries/userAgentUtils/constants.js'; /** * @typedef {import('../src/adapters/bidderFactory').Bid} Bid @@ -410,12 +411,12 @@ function cmerRender(bid) { } /** - * Stop sending push_sync requests in case it's either Safari browser OR iOS device OR GDPR applies. + * Stop sending push_sync requests in case it's either Safari browser OR iOS device OR GDPR applies OR it's bot-like traffic. * Data extracted from navigator's userAgent * @param {Object} gdprConsent Is the GDPR Consent object wrapping gdprApplies {boolean} and consentString {string} attributes. */ function skipSync(gdprConsent) { - return (getBrowser() === browserTypes.SAFARI || getOS() === osTypes.IOS) || gdprApplies(gdprConsent); + return (getBrowser() === browserTypes.SAFARI || getOS() === osTypes.IOS) || gdprApplies(gdprConsent) || isBotLikeTraffic(); } /** @@ -425,4 +426,13 @@ function gdprApplies(gdprConsent) { return gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies; } +/** + * Check if the user agent is bot-like + * @returns {boolean} + */ +function isBotLikeTraffic() { + const botPattern = new RegExp(BOL_LIKE_USER_AGENTS.join('|'), 'i'); + return botPattern.test(navigator.userAgent); +} + registerBidder(spec); diff --git a/package-lock.json b/package-lock.json index 511a844ddc2..2da5c893b64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "prebid.js", - "version": "10.13.0-pre", + "version": "10.18.0-pre", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "10.13.0-pre", + "version": "10.18.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-runtime": "^7.18.9", - "@babel/preset-env": "^7.28.3", + "@babel/preset-env": "^7.27.2", "@babel/preset-typescript": "^7.26.0", "@babel/runtime": "^7.28.3", "core-js": "^3.45.1", @@ -25,15 +25,17 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, "devDependencies": { - "@babel/eslint-parser": "^7.28.4", + "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", - "@eslint/compat": "^1.4.0", - "@types/google-publisher-tag": "^1.20250811.1", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", + "@eslint/compat": "^1.3.1", + "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", "@wdio/cli": "^9.19.1", "@wdio/concise-reporter": "^8.29.0", @@ -46,14 +48,14 @@ "body-parser": "^1.19.0", "chai": "^4.2.0", "deep-equal": "^2.0.3", - "eslint": "^9.35.0", + "eslint": "^9.31.0", "eslint-plugin-chai-friendly": "^1.1.0", - "eslint-plugin-import": "^2.32.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.6", "execa": "^1.0.0", "faker": "^5.5.3", "fancy-log": "^2.0.0", - "fs-extra": "^11.3.2", + "fs-extra": "^11.3.1", "globals": "^16.3.0", "gulp": "^5.0.1", "gulp-clean": "^0.4.0", @@ -94,22 +96,22 @@ "node-html-parser": "^6.1.5", "opn": "^5.4.0", "plugin-error": "^2.0.1", - "puppeteer": "^24.18.0", + "puppeteer": "^24.10.0", "resolve-from": "^5.0.0", "sinon": "^20.0.0", "source-map-loader": "^5.0.0", "through2": "^4.0.2", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1", - "url": "^0.11.4", + "url": "^0.11.0", "url-parse": "^1.0.5", "video.js": "^7.21.7", "videojs-contrib-ads": "^6.9.0", "videojs-ima": "^2.4.0", "videojs-playlist": "^5.2.0", "webdriver": "^9.19.2", - "webdriverio": "^9.20.0", - "webpack": "^5.101.3", + "webdriverio": "^9.18.4", + "webpack": "^5.102.1", "webpack-bundle-analyzer": "^4.5.0", "webpack-manifest-plugin": "^5.0.1", "webpack-stream": "^7.0.0", @@ -138,9 +140,7 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.27.5", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -177,9 +177,7 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.28.4.tgz", - "integrity": "sha512-Aa+yDiH87980jR6zvRfFuCR1+dLb00vBydhTL+zI992Rz/wQhSvuxjmOOuJOgO3XmakO6RykRGD2S1mq1AtgHA==", + "version": "7.24.7", "dev": true, "license": "MIT", "dependencies": { @@ -236,17 +234,15 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.27.1", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -272,44 +268,19 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "version": "0.6.4", "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" + "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -533,13 +504,11 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "version": "7.27.1", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -640,14 +609,12 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "version": "7.27.1", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -685,9 +652,7 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", - "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "version": "7.27.5", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -714,12 +679,10 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "version": "7.27.1", "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { @@ -730,17 +693,15 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "version": "7.27.1", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" + "@babel/traverse": "^7.27.1", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" @@ -749,6 +710,13 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", "license": "MIT", @@ -764,13 +732,10 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "version": "7.27.3", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -833,22 +798,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "license": "MIT", @@ -1068,16 +1017,13 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "version": "7.27.3", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" + "@babel/plugin-transform-destructuring": "^7.27.3", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1128,9 +1074,7 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.27.1", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1185,9 +1129,7 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "version": "7.27.5", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1385,12 +1327,10 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "version": "7.27.2", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", + "@babel/compat-data": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -1398,26 +1338,25 @@ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-block-scoping": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -1434,15 +1373,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-parameters": "^7.27.1", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regenerator": "^7.27.1", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1455,10 +1394,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -1468,19 +1407,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "license": "MIT", @@ -1686,9 +1612,14 @@ "uuid": "9.0.1" } }, + "node_modules/@chiragrupani/karma-chromium-edge-launcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chiragrupani/karma-chromium-edge-launcher/-/karma-chromium-edge-launcher-2.4.1.tgz", + "integrity": "sha512-HwTlN4dk7dnL9m5nEonq7cI3Wa787wYfGVWeb4oWPMySIEhFpA7/BYQ8zMbpQ4YkSQxVnvY1502aWdbI3w7DeA==", + "dev": true + }, "node_modules/@colors/colors": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.1.90" @@ -1750,9 +1681,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -1788,14 +1719,11 @@ } }, "node_modules/@eslint/compat": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz", - "integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.1.tgz", + "integrity": "sha512-k8MHony59I5EPic6EQTCNOuPoVBnoYXkP+20xvwFjN7t0qI3ImyvyBgg+hIVPwC8JaxVjjUZld+cLfBLFDLucg==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -1808,19 +1736,6 @@ } } }, - "node_modules/@eslint/compat/node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/config-array": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", @@ -1837,9 +1752,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1847,9 +1762,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1904,9 +1819,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1917,9 +1832,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", "dev": true, "license": "MIT", "engines": { @@ -1940,13 +1855,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.15.1", "levn": "^0.4.1" }, "engines": { @@ -2936,18 +2851,17 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.10.8", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.8.tgz", - "integrity": "sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==", + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.5.tgz", + "integrity": "sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "debug": "^4.4.1", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.2", - "tar-fs": "^3.1.0", + "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, "bin": { @@ -3080,7 +2994,6 @@ }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", - "dev": true, "license": "MIT" }, "node_modules/@stylistic/eslint-plugin": { @@ -3149,12 +3062,10 @@ }, "node_modules/@types/cookie": { "version": "0.4.1", - "dev": true, "license": "MIT" }, "node_modules/@types/cors": { "version": "2.8.17", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3200,11 +3111,10 @@ "license": "MIT" }, "node_modules/@types/google-publisher-tag": { - "version": "1.20250811.1", - "resolved": "https://registry.npmjs.org/@types/google-publisher-tag/-/google-publisher-tag-1.20250811.1.tgz", - "integrity": "sha512-35rEuUfoCENB5aCt2mQIfLomoSfGtp/DwcAZMQ1dSFyEceXEsz7TmPuRGz4lwoZiSq991ZaDz49MNawtPy5XPw==", - "dev": true, - "license": "MIT" + "version": "1.20250428.0", + "resolved": "https://registry.npmjs.org/@types/google-publisher-tag/-/google-publisher-tag-1.20250428.0.tgz", + "integrity": "sha512-W+aTMsM4e8PE/TkH/RkMbmmwEFg2si9eUugS5/lt88wkEClqcALi+3WLXW39Xgzu89+3igi/RNIpPLKdt6W7Dg==", + "dev": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -3243,7 +3153,6 @@ }, "node_modules/@types/node": { "version": "20.14.2", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -3284,7 +3193,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -3969,24 +3880,6 @@ "@wdio/cli": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/@wdio/config": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.1.tgz", - "integrity": "sha512-BeTB2paSjaij3cf1NXQzX9CZmdj5jz2/xdUhkJlCeGmGn1KjWu5BjMO+exuiy+zln7dOJjev8f0jlg8e8f1EbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@wdio/logger": "9.18.0", - "@wdio/types": "9.19.1", - "@wdio/utils": "9.19.1", - "deepmerge-ts": "^7.0.3", - "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, "node_modules/@wdio/browserstack-service/node_modules/@wdio/logger": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.18.0.tgz", @@ -4004,26 +3897,6 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/@wdio/protocols": { - "version": "9.16.2", - "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.16.2.tgz", - "integrity": "sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==", - "dev": true, - "license": "MIT" - }, - "node_modules/@wdio/browserstack-service/node_modules/@wdio/repl": { - "version": "9.16.2", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", - "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.1.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, "node_modules/@wdio/browserstack-service/node_modules/@wdio/reporter": { "version": "9.19.1", "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.19.1.tgz", @@ -4054,42 +3927,6 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/browserstack-service/node_modules/@wdio/utils": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.1.tgz", - "integrity": "sha512-wWx5uPCgdZQxFIemAFVk/aa3JLwqrTsvEJsPlV3lCRpLeQ67V8aUPvvNAzE+RhX67qvelwwsvX8RrPdLDfnnYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@puppeteer/browsers": "^2.2.0", - "@wdio/logger": "9.18.0", - "@wdio/types": "9.19.1", - "decamelize": "^6.0.0", - "deepmerge-ts": "^7.0.3", - "edgedriver": "^6.1.2", - "geckodriver": "^5.0.0", - "get-port": "^7.0.0", - "import-meta-resolve": "^4.0.0", - "locate-app": "^2.2.24", - "mitt": "^3.0.1", - "safaridriver": "^1.0.0", - "split2": "^4.2.0", - "wait-port": "^1.1.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, - "node_modules/@wdio/browserstack-service/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/@wdio/browserstack-service/node_modules/chalk": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", @@ -4113,20 +3950,6 @@ "node": ">=0.3.1" } }, - "node_modules/@wdio/browserstack-service/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@wdio/browserstack-service/node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -4141,74 +3964,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/@wdio/browserstack-service/node_modules/webdriver": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.1.tgz", - "integrity": "sha512-cvccIZ3QaUZxxrA81a3rqqgxKt6VzVrZupMc+eX9J40qfGrV3NtdLb/m4AA1PmeTPGN5O3/4KrzDpnVZM4WUnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.1.0", - "@types/ws": "^8.5.3", - "@wdio/config": "9.19.1", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.16.2", - "@wdio/types": "9.19.1", - "@wdio/utils": "9.19.1", - "deepmerge-ts": "^7.0.3", - "https-proxy-agent": "^7.0.6", - "undici": "^6.21.3", - "ws": "^8.8.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, - "node_modules/@wdio/browserstack-service/node_modules/webdriverio": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.19.1.tgz", - "integrity": "sha512-hpGgK6d9QNi3AaLFWIPQaEMqJhXF048XAIsV5i5mkL0kjghV1opcuhKgbbG+7pcn8JSpiq6mh7o3MDYtapw90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.11.30", - "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.19.1", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.16.2", - "@wdio/repl": "9.16.2", - "@wdio/types": "9.19.1", - "@wdio/utils": "9.19.1", - "archiver": "^7.0.1", - "aria-query": "^5.3.0", - "cheerio": "^1.0.0-rc.12", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "grapheme-splitter": "^1.0.4", - "htmlfy": "^0.8.1", - "is-plain-obj": "^4.1.0", - "jszip": "^3.10.1", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "query-selector-shadow-dom": "^1.0.1", - "resq": "^1.11.0", - "rgb2hex": "0.2.5", - "serialize-error": "^12.0.0", - "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.19.1" - }, - "engines": { - "node": ">=18.20.0" - }, - "peerDependencies": { - "puppeteer-core": ">=22.x || <=24.x" - }, - "peerDependenciesMeta": { - "puppeteer-core": { - "optional": true - } - } - }, "node_modules/@wdio/cli": { "version": "9.19.1", "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.19.1.tgz", @@ -4413,19 +4168,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@wdio/cli/node_modules/@wdio/repl": { - "version": "9.16.2", - "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.16.2.tgz", - "integrity": "sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.1.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, "node_modules/@wdio/cli/node_modules/@wdio/types": { "version": "9.19.1", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.1.tgz", @@ -4465,16 +4207,6 @@ "node": ">=18.20.0" } }, - "node_modules/@wdio/cli/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/@wdio/cli/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -4634,20 +4366,6 @@ "node": ">=8" } }, - "node_modules/@wdio/cli/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@wdio/cli/node_modules/jest-diff": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", @@ -4923,98 +4641,30 @@ "engines": { "node": ">=8" } - }, - "node_modules/@wdio/cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wdio/cli/node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@wdio/cli/node_modules/webdriver": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.1.tgz", - "integrity": "sha512-cvccIZ3QaUZxxrA81a3rqqgxKt6VzVrZupMc+eX9J40qfGrV3NtdLb/m4AA1PmeTPGN5O3/4KrzDpnVZM4WUnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.1.0", - "@types/ws": "^8.5.3", - "@wdio/config": "9.19.1", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.16.2", - "@wdio/types": "9.19.1", - "@wdio/utils": "9.19.1", - "deepmerge-ts": "^7.0.3", - "https-proxy-agent": "^7.0.6", - "undici": "^6.21.3", - "ws": "^8.8.0" - }, - "engines": { - "node": ">=18.20.0" - } - }, - "node_modules/@wdio/cli/node_modules/webdriverio": { - "version": "9.19.1", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.19.1.tgz", - "integrity": "sha512-hpGgK6d9QNi3AaLFWIPQaEMqJhXF048XAIsV5i5mkL0kjghV1opcuhKgbbG+7pcn8JSpiq6mh7o3MDYtapw90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^20.11.30", - "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.19.1", - "@wdio/logger": "9.18.0", - "@wdio/protocols": "9.16.2", - "@wdio/repl": "9.16.2", - "@wdio/types": "9.19.1", - "@wdio/utils": "9.19.1", - "archiver": "^7.0.1", - "aria-query": "^5.3.0", - "cheerio": "^1.0.0-rc.12", - "css-shorthand-properties": "^1.1.1", - "css-value": "^0.0.1", - "grapheme-splitter": "^1.0.4", - "htmlfy": "^0.8.1", - "is-plain-obj": "^4.1.0", - "jszip": "^3.10.1", - "lodash.clonedeep": "^4.5.0", - "lodash.zip": "^4.2.0", - "query-selector-shadow-dom": "^1.0.1", - "resq": "^1.11.0", - "rgb2hex": "0.2.5", - "serialize-error": "^12.0.0", - "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.19.1" + }, + "node_modules/@wdio/cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" }, "engines": { - "node": ">=18.20.0" - }, - "peerDependencies": { - "puppeteer-core": ">=22.x || <=24.x" - }, - "peerDependenciesMeta": { - "puppeteer-core": { - "optional": true - } + "node": ">=8" + } + }, + "node_modules/@wdio/cli/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" } }, "node_modules/@wdio/cli/node_modules/yargs": { @@ -6458,7 +6108,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6484,7 +6133,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6743,20 +6391,16 @@ "license": "MIT" }, "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "version": "3.1.8", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -6807,19 +6451,16 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "version": "1.2.5", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", + "es-abstract": "^1.23.2", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6829,16 +6470,14 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "version": "1.3.2", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7015,9 +6654,9 @@ } }, "node_modules/axios": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz", - "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, "license": "MIT", "dependencies": { @@ -7085,13 +6724,11 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "version": "0.4.11", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" }, "peerDependencies": { @@ -7100,7 +6737,6 @@ }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.11.1", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", @@ -7111,12 +6747,10 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "version": "0.6.2", "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -7137,7 +6771,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -7228,16 +6861,15 @@ }, "node_modules/base64id": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz", + "integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -7282,7 +6914,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7425,7 +7056,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7434,7 +7064,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7449,9 +7078,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "funding": [ { "type": "opencollective", @@ -7468,9 +7097,9 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, @@ -7643,9 +7272,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001746", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz", - "integrity": "sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==", + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", "funding": [ { "type": "opencollective", @@ -7762,7 +7391,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7792,11 +7420,10 @@ } }, "node_modules/chromium-bidi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-8.0.0.tgz", - "integrity": "sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-5.1.0.tgz", + "integrity": "sha512-9MSRhWRVoRPDG0TgzkHrshFSJJNZzfY5UFqUMuksg7zL1yoZIZ3jLB0YAgHclbiAxPI86pBnwDX1tbzoiV8aFw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" @@ -8088,7 +7715,6 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/concat-with-sourcemaps": { @@ -8101,7 +7727,6 @@ }, "node_modules/connect": { "version": "3.7.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -8123,7 +7748,6 @@ }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -8131,7 +7755,6 @@ }, "node_modules/connect/node_modules/finalhandler": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -8148,12 +7771,10 @@ }, "node_modules/connect/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -8164,7 +7785,6 @@ }, "node_modules/connect/node_modules/statuses": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8240,12 +7860,10 @@ } }, "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "version": "3.42.0", "license": "MIT", "dependencies": { - "browserslist": "^4.25.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -8258,7 +7876,6 @@ }, "node_modules/cors": { "version": "2.8.5", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -8301,10 +7918,11 @@ "dev": true }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -8745,7 +8363,6 @@ }, "node_modules/custom-event": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/d": { @@ -8818,7 +8435,6 @@ }, "node_modules/date-format": { "version": "4.0.14", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0" @@ -9090,15 +8706,13 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1475386", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", - "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", - "dev": true, - "license": "BSD-3-Clause" + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "dev": true }, "node_modules/di": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/diff": { @@ -9136,7 +8750,6 @@ }, "node_modules/dom-serialize": { "version": "2.2.1", - "dev": true, "license": "MIT", "dependencies": { "custom-event": "~1.0.0", @@ -9414,14 +9027,13 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -9434,7 +9046,6 @@ }, "node_modules/encodeurl": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9473,7 +9084,6 @@ }, "node_modules/engine.io": { "version": "6.6.2", - "dev": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -9493,7 +9103,6 @@ }, "node_modules/engine.io-parser": { "version": "5.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9501,7 +9110,6 @@ }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9523,7 +9131,6 @@ }, "node_modules/ent": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/entities": { @@ -9573,9 +9180,7 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.23.9", "dev": true, "license": "MIT", "dependencies": { @@ -9583,18 +9188,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", + "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -9606,24 +9211,21 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", - "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", + "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", + "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -9632,7 +9234,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -9732,16 +9334,11 @@ } }, "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "version": "1.0.2", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -9964,20 +9561,20 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -10157,9 +9754,7 @@ "license": "MIT" }, "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "version": "2.12.0", "dev": true, "license": "MIT", "dependencies": { @@ -10176,8 +9771,6 @@ }, "node_modules/eslint-module-utils/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10220,30 +9813,28 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "version": "2.31.0", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", + "eslint-module-utils": "^2.12.0", "hasown": "^2.0.2", - "is-core-module": "^2.16.1", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.1", + "object.values": "^1.2.0", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -10906,7 +10497,6 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "dev": true, "license": "MIT" }, "node_modules/events": { @@ -11141,7 +10731,6 @@ }, "node_modules/extend": { "version": "3.0.2", - "dev": true, "license": "MIT" }, "node_modules/extend-shallow": { @@ -11421,7 +11010,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11551,12 +11139,10 @@ }, "node_modules/flatted": { "version": "3.3.1", - "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", - "dev": true, "funding": [ { "type": "individual", @@ -11697,9 +11283,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", "dev": true, "license": "MIT", "dependencies": { @@ -11732,7 +11318,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11865,7 +11450,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -12014,7 +11598,9 @@ "license": "ISC" }, "node_modules/glob": { - "version": "10.4.5", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -12034,7 +11620,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12074,6 +11659,8 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, "license": "BSD-2-Clause" }, @@ -13122,7 +12709,6 @@ }, "node_modules/http-proxy": { "version": "1.18.1", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -13295,7 +12881,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -13467,7 +13052,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13526,9 +13110,7 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.15.1", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -13607,7 +13189,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13629,7 +13210,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13656,7 +13236,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13699,22 +13278,8 @@ "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13968,7 +13533,6 @@ }, "node_modules/isbinaryfile": { "version": "4.0.10", - "dev": true, "license": "MIT", "engines": { "node": ">= 8.0.0" @@ -14825,22 +14389,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -14964,7 +14520,6 @@ "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", - "dev": true, "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", @@ -15248,8 +14803,17 @@ }, "node_modules/karma-safari-launcher": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", + "integrity": "sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==", "dev": true, - "license": "MIT", + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-safarinative-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-safarinative-launcher/-/karma-safarinative-launcher-1.1.0.tgz", + "integrity": "sha512-vdMjdQDHkSUbOZc8Zq2K5bBC0yJGFEgfrKRJTqt0Um0SC1Rt8drS2wcN6UA3h4LgsL3f1pMcmRSvKucbJE8Qdg==", "peerDependencies": { "karma": ">=0.9" } @@ -15366,7 +14930,6 @@ }, "node_modules/karma/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -15380,7 +14943,6 @@ }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -15390,7 +14952,6 @@ }, "node_modules/karma/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -15401,12 +14962,10 @@ }, "node_modules/karma/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/karma/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -15425,7 +14984,6 @@ }, "node_modules/karma/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15436,7 +14994,6 @@ }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -15452,7 +15009,6 @@ }, "node_modules/karma/node_modules/yargs": { "version": "16.2.0", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -15469,7 +15025,6 @@ }, "node_modules/karma/node_modules/yargs-parser": { "version": "20.2.9", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -15732,7 +15287,6 @@ }, "node_modules/log4js": { "version": "6.9.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "date-format": "^4.0.14", @@ -15953,7 +15507,6 @@ }, "node_modules/mime": { "version": "2.6.0", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -15980,15 +15533,17 @@ } }, "node_modules/min-document": { - "version": "2.19.0", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", "dev": true, + "license": "MIT", "dependencies": { "dom-walk": "^0.1.0" } }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -15999,7 +15554,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16021,7 +15575,6 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -16211,7 +15764,9 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -16745,9 +16300,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", "license": "MIT" }, "node_modules/node-request-interceptor": { @@ -16810,7 +16365,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16859,7 +16413,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17027,7 +16580,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -17344,7 +16896,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17448,7 +16999,6 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -17736,18 +17286,17 @@ } }, "node_modules/puppeteer": { - "version": "24.18.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.18.0.tgz", - "integrity": "sha512-Ke8oL/87GhzKIM2Ag6Yj49t5xbGc4rspGIuSuFLFCQBtYzWqCSanvqoCu08WkI78rbqcwnHjxiTH6oDlYFrjrw==", + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.11.2.tgz", + "integrity": "sha512-HopdRZWHa5zk0HSwd8hU+GlahQ3fmesTAqMIDHVY9HasCvppcYuHYXyjml0nlm+nbwVCqAQWV+dSmiNCrZGTGQ==", "dev": true, "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.8", - "chromium-bidi": "8.0.0", + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1475386", - "puppeteer-core": "24.18.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.11.2", "typed-query-selector": "^2.12.0" }, "bin": { @@ -17758,16 +17307,15 @@ } }, "node_modules/puppeteer-core": { - "version": "24.18.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.18.0.tgz", - "integrity": "sha512-As0BvfXxek2MbV0m7iqBmQKFnfSrzSvTM4zGipjd4cL+9f2Ccgut6RvHlc8+qBieKHqCMFy9BSI4QyveoYXTug==", + "version": "24.11.2", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.11.2.tgz", + "integrity": "sha512-c49WifNb8hix+gQH17TldmD6TC/Md2HBaTJLHexIUq4sZvo2pyHY/Pp25qFQjibksBu/SJRYUY7JsoaepNbiRA==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.10.8", - "chromium-bidi": "8.0.0", + "@puppeteer/browsers": "2.10.5", + "chromium-bidi": "5.1.0", "debug": "^4.4.1", - "devtools-protocol": "0.0.1475386", + "devtools-protocol": "0.0.1464554", "typed-query-selector": "^2.12.0", "ws": "^8.18.3" }, @@ -17830,7 +17378,6 @@ }, "node_modules/qjobs": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.9" @@ -18063,7 +17610,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -18222,7 +17768,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18238,25 +17783,19 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.8", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -18334,7 +17873,6 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "dev": true, "license": "MIT" }, "node_modules/rgb2hex": { @@ -18344,7 +17882,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -18358,7 +17895,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -18537,9 +18073,9 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -19106,7 +18642,6 @@ }, "node_modules/socket.io": { "version": "4.8.0", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -19123,7 +18658,6 @@ }, "node_modules/socket.io-adapter": { "version": "2.5.5", - "dev": true, "license": "MIT", "dependencies": { "debug": "~4.3.4", @@ -19132,7 +18666,6 @@ }, "node_modules/socket.io-parser": { "version": "4.2.4", - "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -19183,7 +18716,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -19368,14 +18900,11 @@ } }, "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "version": "1.0.0", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" + "internal-slot": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -19443,7 +18972,6 @@ }, "node_modules/streamroller": { "version": "3.1.5", - "dev": true, "license": "MIT", "dependencies": { "date-format": "^4.0.14", @@ -19456,7 +18984,6 @@ }, "node_modules/streamroller/node_modules/fs-extra": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -19469,7 +18996,6 @@ }, "node_modules/streamroller/node_modules/jsonfile": { "version": "4.0.0", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -19477,7 +19003,6 @@ }, "node_modules/streamroller/node_modules/universalify": { "version": "0.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -19517,7 +19042,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19555,7 +19079,6 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19801,11 +19324,17 @@ } }, "node_modules/tapable": { - "version": "2.2.1", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar-fs": { @@ -20135,7 +19664,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -20156,7 +19684,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -20477,7 +20004,6 @@ }, "node_modules/ua-parser-js": { "version": "0.7.38", - "dev": true, "funding": [ { "type": "opencollective", @@ -20574,7 +20100,6 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -20709,17 +20234,12 @@ } }, "node_modules/url": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", - "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "version": "0.11.3", "dev": true, "license": "MIT", "dependencies": { "punycode": "^1.4.1", - "qs": "^6.12.3" - }, - "engines": { - "node": ">= 0.4" + "qs": "^6.11.2" } }, "node_modules/url-parse": { @@ -21059,7 +20579,6 @@ }, "node_modules/void-elements": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21154,7 +20673,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.1", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { @@ -21324,20 +20845,20 @@ } }, "node_modules/webdriverio": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.20.0.tgz", - "integrity": "sha512-cqaXfahTzCFaQLlk++feZaze6tAsW8OSdaVRgmOGJRII1z2A4uh4YGHtusTpqOiZAST7OBPqycOwfh01G/Ktbg==", + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.19.1.tgz", + "integrity": "sha512-hpGgK6d9QNi3AaLFWIPQaEMqJhXF048XAIsV5i5mkL0kjghV1opcuhKgbbG+7pcn8JSpiq6mh7o3MDYtapw90w==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "^20.11.30", "@types/sinonjs__fake-timers": "^8.1.5", - "@wdio/config": "9.20.0", + "@wdio/config": "9.19.1", "@wdio/logger": "9.18.0", "@wdio/protocols": "9.16.2", "@wdio/repl": "9.16.2", - "@wdio/types": "9.20.0", - "@wdio/utils": "9.20.0", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", "archiver": "^7.0.1", "aria-query": "^5.3.0", "cheerio": "^1.0.0-rc.12", @@ -21354,7 +20875,7 @@ "rgb2hex": "0.2.5", "serialize-error": "^12.0.0", "urlpattern-polyfill": "^10.0.0", - "webdriver": "9.20.0" + "webdriver": "9.19.1" }, "engines": { "node": ">=18.20.0" @@ -21369,19 +20890,18 @@ } }, "node_modules/webdriverio/node_modules/@wdio/config": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.20.0.tgz", - "integrity": "sha512-ggwd3EMsVj/LTcbYw2h+hma+/7fQ1cTXMuy9B5WTkLjDlOtbLjsqs9QLt4BLIo1cdsxvAw/UVpRVUuYy7rTmtQ==", + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.19.1.tgz", + "integrity": "sha512-BeTB2paSjaij3cf1NXQzX9CZmdj5jz2/xdUhkJlCeGmGn1KjWu5BjMO+exuiy+zln7dOJjev8f0jlg8e8f1EbQ==", "dev": true, "license": "MIT", "dependencies": { "@wdio/logger": "9.18.0", - "@wdio/types": "9.20.0", - "@wdio/utils": "9.20.0", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", "deepmerge-ts": "^7.0.3", "glob": "^10.2.2", - "import-meta-resolve": "^4.0.0", - "jiti": "^2.5.1" + "import-meta-resolve": "^4.0.0" }, "engines": { "node": ">=18.20.0" @@ -21425,9 +20945,9 @@ } }, "node_modules/webdriverio/node_modules/@wdio/types": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.20.0.tgz", - "integrity": "sha512-zMmAtse2UMCSOW76mvK3OejauAdcFGuKopNRH7crI0gwKTZtvV89yXWRziz9cVXpFgfmJCjf9edxKFWdhuF5yw==", + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.19.1.tgz", + "integrity": "sha512-Q1HVcXiWMHp3ze2NN1BvpsfEh/j6GtAeMHhHW4p2IWUfRZlZqTfiJ+95LmkwXOG2gw9yndT8NkJigAz8v7WVYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21438,15 +20958,15 @@ } }, "node_modules/webdriverio/node_modules/@wdio/utils": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.20.0.tgz", - "integrity": "sha512-T1ze005kncUTocYImSBQc/FAVcOwP/vOU4MDJFgzz/RTcps600qcKX98sVdWM5/ukXCVkjOufWteDHIbX5/tEA==", + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.19.1.tgz", + "integrity": "sha512-wWx5uPCgdZQxFIemAFVk/aa3JLwqrTsvEJsPlV3lCRpLeQ67V8aUPvvNAzE+RhX67qvelwwsvX8RrPdLDfnnYw==", "dev": true, "license": "MIT", "dependencies": { "@puppeteer/browsers": "^2.2.0", "@wdio/logger": "9.18.0", - "@wdio/types": "9.20.0", + "@wdio/types": "9.19.1", "decamelize": "^6.0.0", "deepmerge-ts": "^7.0.3", "edgedriver": "^6.1.2", @@ -21474,9 +20994,9 @@ } }, "node_modules/webdriverio/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", "dev": true, "license": "MIT", "engines": { @@ -21501,19 +21021,19 @@ } }, "node_modules/webdriverio/node_modules/webdriver": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.20.0.tgz", - "integrity": "sha512-Kk+AGV1xWLNHVpzUynQJDULMzbcO3IjXo3s0BzfC30OpGxhpaNmoazMQodhtv0Lp242Mb1VYXD89dCb4oAHc4w==", + "version": "9.19.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.19.1.tgz", + "integrity": "sha512-cvccIZ3QaUZxxrA81a3rqqgxKt6VzVrZupMc+eX9J40qfGrV3NtdLb/m4AA1PmeTPGN5O3/4KrzDpnVZM4WUnA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "^20.1.0", "@types/ws": "^8.5.3", - "@wdio/config": "9.20.0", + "@wdio/config": "9.19.1", "@wdio/logger": "9.18.0", "@wdio/protocols": "9.16.2", - "@wdio/types": "9.20.0", - "@wdio/utils": "9.20.0", + "@wdio/types": "9.19.1", + "@wdio/utils": "9.19.1", "deepmerge-ts": "^7.0.3", "https-proxy-agent": "^7.0.6", "undici": "^6.21.3", @@ -21524,9 +21044,9 @@ } }, "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "version": "5.102.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", + "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -21538,7 +21058,7 @@ "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", + "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", @@ -21550,10 +21070,10 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", + "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { @@ -22112,12 +21632,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.17.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -22144,7 +21662,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index ee096b18146..439d82f77af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "10.13.0-pre", + "version": "10.18.0-pre", "description": "Header Bidding Management Library", "main": "dist/src/prebid.public.ts", "exports": { @@ -53,11 +53,12 @@ "node": ">=20.0.0" }, "devDependencies": { - "@babel/eslint-parser": "^7.28.4", + "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", - "@eslint/compat": "^1.4.0", - "@types/google-publisher-tag": "^1.20250811.1", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", + "@eslint/compat": "^1.3.1", + "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", "@wdio/cli": "^9.19.1", "@wdio/concise-reporter": "^8.29.0", @@ -70,14 +71,14 @@ "body-parser": "^1.19.0", "chai": "^4.2.0", "deep-equal": "^2.0.3", - "eslint": "^9.35.0", + "eslint": "^9.31.0", "eslint-plugin-chai-friendly": "^1.1.0", - "eslint-plugin-import": "^2.32.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsdoc": "^50.6.6", "execa": "^1.0.0", "faker": "^5.5.3", "fancy-log": "^2.0.0", - "fs-extra": "^11.3.2", + "fs-extra": "^11.3.1", "globals": "^16.3.0", "gulp": "^5.0.1", "gulp-clean": "^0.4.0", @@ -118,22 +119,22 @@ "node-html-parser": "^6.1.5", "opn": "^5.4.0", "plugin-error": "^2.0.1", - "puppeteer": "^24.18.0", + "puppeteer": "^24.10.0", "resolve-from": "^5.0.0", "sinon": "^20.0.0", "source-map-loader": "^5.0.0", "through2": "^4.0.2", "typescript": "^5.8.2", "typescript-eslint": "^8.26.1", - "url": "^0.11.4", + "url": "^0.11.0", "url-parse": "^1.0.5", "video.js": "^7.21.7", "videojs-contrib-ads": "^6.9.0", "videojs-ima": "^2.4.0", "videojs-playlist": "^5.2.0", "webdriver": "^9.19.2", - "webdriverio": "^9.20.0", - "webpack": "^5.101.3", + "webdriverio": "^9.18.4", + "webpack": "^5.102.1", "webpack-bundle-analyzer": "^4.5.0", "webpack-manifest-plugin": "^5.0.1", "webpack-stream": "^7.0.0", @@ -142,7 +143,7 @@ "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-runtime": "^7.18.9", - "@babel/preset-env": "^7.28.3", + "@babel/preset-env": "^7.27.2", "@babel/preset-typescript": "^7.26.0", "@babel/runtime": "^7.28.3", "core-js": "^3.45.1", @@ -156,6 +157,7 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, diff --git a/src/activities/redactor.ts b/src/activities/redactor.ts index 30ce6e5636b..d58dd3588ce 100644 --- a/src/activities/redactor.ts +++ b/src/activities/redactor.ts @@ -134,7 +134,7 @@ function bidRequestTransmitRules(isAllowed = isActivityAllowed) { }, { name: ACTIVITY_TRANSMIT_TID, - paths: ['ortb2Imp.ext.tid'], + paths: ['ortb2Imp.ext.tid', 'ortb2Imp.ext.tidSource'], applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_TID, isAllowed) } ].map(redactRule) @@ -178,7 +178,7 @@ export function ortb2TransmitRules(isAllowed = isActivityAllowed) { }, { name: ACTIVITY_TRANSMIT_TID, - paths: ['source.tid'], + paths: ['source.tid', 'source.ext.tidSource'], applies: appliesWhenActivityDenied(ACTIVITY_TRANSMIT_TID, isAllowed), } ].map(redactRule); @@ -214,6 +214,11 @@ declare module '../config' { * privacy concern. Since version 8 they are disabled by default, and can be re-enabled with this flag. */ enableTIDs?: boolean; + /** + * When enabled alongside enableTIDs, bidders receive a consistent source.tid for an auction rather than + * bidder-specific values. + */ + consistentTIDs?: boolean; } } // by default, TIDs are off since version 8 diff --git a/src/adRendering.ts b/src/adRendering.ts index 00ce5ac5b9d..b35959c75e0 100644 --- a/src/adRendering.ts +++ b/src/adRendering.ts @@ -351,12 +351,24 @@ export function renderAdDirect(doc, adId, options) { } const messageHandler = creativeMessageHandler({resizeFn}); + function waitForDocumentReady(doc) { + return new PbPromise((resolve) => { + if (doc.readyState === 'loading') { + doc.addEventListener('DOMContentLoaded', resolve); + } else { + resolve(); + } + }) + } + function renderFn(adData) { - getCreativeRenderer(bid) - .then(render => render(adData, { - sendMessage: (type, data) => messageHandler(type, data, bid), - mkFrame: createIframe, - }, doc.defaultView)) + PbPromise.all([ + getCreativeRenderer(bid), + waitForDocumentReady(doc) + ]).then(([render]) => render(adData, { + sendMessage: (type, data) => messageHandler(type, data, bid), + mkFrame: createIframe, + }, doc.defaultView)) .then( () => emitAdRenderSucceeded({doc, bid, id: bid.adId}), (e) => { diff --git a/src/adapterManager.ts b/src/adapterManager.ts index a438fa06bcd..d19a43fcf03 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -67,6 +67,7 @@ import type { AnalyticsConfig, AnalyticsProvider, AnalyticsProviderConfig, } from "../libraries/analyticsAdapter/AnalyticsAdapter.ts"; +import {getGlobal} from "./prebidGlobal.ts"; export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; @@ -168,6 +169,7 @@ export interface BaseBidderRequest { */ bidderRequestId: Identifier; auctionId: Identifier; + pageViewId: Identifier; /** * The bidder associated with this request, or null in the case of stored impressions. */ @@ -220,7 +222,7 @@ type GetBidsOptions = { adUnits: (SRC extends typeof S2S.SRC ? PBSAdUnit : AdUnit)[] src: SRC; metrics: Metrics, - tids: { [bidderCode: BidderCode]: string }; + getTid: ReturnType; } export type AliasBidderOptions = { @@ -244,7 +246,7 @@ export type AnalyticsAdapter

= StorageDisclosure & gvlid?: number | ((config: AnalyticsConfig

) => number); } -function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics, tids}: GetBidsOptions): BidRequest[] { +function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics, getTid}: GetBidsOptions): BidRequest[] { return adUnits.reduce((result, adUnit) => { const bids = adUnit.bids.filter(bid => bid.bidder === bidderCode); if (bidderCode == null && bids.length === 0 && (adUnit as PBSAdUnit).s2sBid != null) { @@ -252,11 +254,15 @@ function getBids({bidde } result.push( bids.reduce((bids: BidRequest[], bid: BidRequest) => { - if (!tids.hasOwnProperty(adUnit.transactionId)) { - tids[adUnit.transactionId] = generateUUID(); - } + const [tid, tidSource] = getTid(bid.bidder, adUnit.transactionId, bid.ortb2Imp?.ext?.tid ?? adUnit.ortb2Imp?.ext?.tid); bid = Object.assign({}, bid, - {ortb2Imp: mergeDeep({}, adUnit.ortb2Imp, bid.ortb2Imp, {ext: {tid: tids[adUnit.transactionId]}})}, + { + ortb2Imp: mergeDeep( + {}, + adUnit.ortb2Imp, + bid.ortb2Imp, + {ext: {tid, tidSource}}) + }, getDefinedParams(adUnit, ADUNIT_BID_PROPERTIES), ); @@ -440,6 +446,35 @@ declare module './hook' { } } +function tidFactory() { + const consistent = !!config.getConfig('consistentTIDs'); + let tidSource, getTid; + if (consistent) { + tidSource = 'pbjsStable'; + getTid = (saneTid) => saneTid + } else { + tidSource = 'pbjs'; + getTid = (() => { + const tids = {}; + return (saneTid, bidderCode) => { + if (!tids.hasOwnProperty(bidderCode)) { + tids[bidderCode] = {}; + } + if (!tids[bidderCode].hasOwnProperty(saneTid)) { + tids[bidderCode][saneTid] = `u${generateUUID()}`; + } + return tids[bidderCode][saneTid]; + } + })(); + } + return function (bidderCode, saneTid, fpdTid) { + return [ + fpdTid ?? getTid(saneTid, bidderCode), + fpdTid != null ? 'pub' : tidSource + ] + } +} + const adapterManager = { bidderRegistry: _bidderRegistry, analyticsRegistry: _analyticsRegistry, @@ -493,16 +528,7 @@ const adapterManager = { const ortb2 = ortb2Fragments.global || {}; const bidderOrtb2 = ortb2Fragments.bidder || {}; - const sourceTids: any = {}; - const extTids: any = {}; - - function tidFor(tids, bidderCode, makeTid) { - const tid = tids.hasOwnProperty(bidderCode) ? tids[bidderCode] : makeTid(); - if (bidderCode != null) { - tids[bidderCode] = tid; - } - return tid; - } + const getTid = tidFactory(); function addOrtb2>(bidderRequest: Partial, s2sActivityParams?): T { const redact = dep.redact( @@ -510,12 +536,17 @@ const adapterManager = { ? s2sActivityParams : activityParams(MODULE_TYPE_BIDDER, bidderRequest.bidderCode) ); - const tid = tidFor(sourceTids, bidderRequest.bidderCode, generateUUID); + const [tid, tidSource] = getTid(bidderRequest.bidderCode, bidderRequest.auctionId, bidderOrtb2[bidderRequest.bidderCode]?.source?.tid ?? ortb2.source?.tid); const fpd = Object.freeze(redact.ortb2(mergeDeep( {}, ortb2, bidderOrtb2[bidderRequest.bidderCode], - {source: {tid}} + { + source: { + tid, + ext: {tidSource} + } + } ))); bidderRequest.ortb2 = fpd; bidderRequest.bids = bidderRequest.bids.map((bid) => { @@ -525,6 +556,15 @@ const adapterManager = { return bidderRequest as T; } + const pbjsInstance = getGlobal(); + + function getPageViewIdForBidder(bidderCode: string | null): string { + if (!pbjsInstance.pageViewIdPerBidder.has(bidderCode)) { + pbjsInstance.pageViewIdPerBidder.set(bidderCode, generateUUID()); + } + return pbjsInstance.pageViewIdPerBidder.get(bidderCode); + } + _s2sConfigs.forEach(s2sConfig => { const s2sParams = s2sActivityParams(s2sConfig); if (s2sConfig && s2sConfig.enabled && dep.isAllowed(ACTIVITY_FETCH_BIDS, s2sParams)) { @@ -534,13 +574,14 @@ const adapterManager = { const uniquePbsTid = generateUUID(); (serverBidders.length === 0 && hasModuleBids ? [null] : serverBidders).forEach(bidderCode => { - const tids = tidFor(extTids, bidderCode, () => ({})); const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ bidderCode, auctionId, bidderRequestId, + pageViewId, uniquePbsTid, bids: getBids({ bidderCode, @@ -549,7 +590,7 @@ const adapterManager = { 'adUnits': deepClone(adUnitsS2SCopy), src: S2S.SRC, metrics, - tids + getTid, }), auctionStart: auctionStart, timeout: s2sConfig.timeout, @@ -582,12 +623,13 @@ const adapterManager = { // client adapters const adUnitsClientCopy = getAdUnitCopyForClientAdapters(adUnits); clientBidders.forEach(bidderCode => { - const tids = tidFor(extTids, bidderCode, () => ({})); const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ bidderCode, auctionId, + pageViewId, bidderRequestId, bids: getBids({ bidderCode, @@ -596,7 +638,7 @@ const adapterManager = { 'adUnits': deepClone(adUnitsClientCopy), src: 'client', metrics, - tids + getTid, }), auctionStart: auctionStart, timeout: cbTimeout, diff --git a/src/adloader.js b/src/adloader.js index 0a8e9b46669..098b78c211b 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -37,6 +37,7 @@ const _approvedLoadExternalJSList = [ 'nodalsAi', 'anonymised', 'optable', + 'oftmedia', // UserId Submodules 'justtag', 'tncId', diff --git a/src/consentHandler.ts b/src/consentHandler.ts index b69f9df0a1a..b3fa9594673 100644 --- a/src/consentHandler.ts +++ b/src/consentHandler.ts @@ -108,12 +108,19 @@ export class ConsentHandler { } getConsentData(): T { - return this.#data; + if (this.#enabled) { + return this.#data; + } + return null; } get hash() { if (this.#dirty) { - this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data)) + this.#hash = cyrb53Hash( + JSON.stringify( + this.#data && this.hashFields ? this.hashFields.map((f) => this.#data[f]) : this.#data + ) + ); this.#dirty = false; } return this.#hash; @@ -132,16 +139,18 @@ class UspConsentHandler extends ConsentHandler> { - hashFields = ['gdprApplies', 'consentString'] + hashFields = ["gdprApplies", "consentString"]; getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { return { gdprApplies: consentData.gdprApplies as boolean, - consentStringSize: (isStr(consentData.vendorData.tcString)) ? consentData.vendorData.tcString.length : 0, + consentStringSize: isStr(consentData.vendorData.tcString) + ? consentData.vendorData.tcString.length + : 0, generatedAt: this.generatedTime, - apiVersion: consentData.apiVersion - } + apiVersion: consentData.apiVersion, + }; } } } diff --git a/src/prebid.ts b/src/prebid.ts index 4ca96f1bd1d..3d5883cf294 100644 --- a/src/prebid.ts +++ b/src/prebid.ts @@ -102,6 +102,7 @@ declare module './prebidGlobal' { */ delayPrerendering?: boolean adUnits: AdUnitDefinition[]; + pageViewIdPerBidder: Map } } @@ -113,6 +114,7 @@ logInfo('Prebid.js v$prebid.version$ loaded'); // create adUnit array pbjsInstance.adUnits = pbjsInstance.adUnits || []; +pbjsInstance.pageViewIdPerBidder = pbjsInstance.pageViewIdPerBidder || new Map(); function validateSizes(sizes, targLength?: number) { let cleanSizes = []; @@ -156,7 +158,7 @@ export function syncOrtb2(adUnit, mediaType) { deepSetValue(adUnit, `mediaTypes.${mediaType}.${key}`, ortbFieldValue); } else if (ortbFieldValue === undefined) { deepSetValue(adUnit, `ortb2Imp.${mediaType}.${key}`, mediaTypesFieldValue); - } else { + } else if (!deepEqual(mediaTypesFieldValue, ortbFieldValue)) { logWarn(`adUnit ${adUnit.code}: specifies conflicting ortb2Imp.${mediaType}.${key} and mediaTypes.${mediaType}.${key}, the latter will be ignored`, adUnit); deepSetValue(adUnit, `mediaTypes.${mediaType}.${key}`, ortbFieldValue); } @@ -483,6 +485,7 @@ declare module './prebidGlobal' { setBidderConfig: typeof config.setBidderConfig; processQueue: typeof processQueue; triggerBilling: typeof triggerBilling; + refreshPageViewId: typeof refreshPageViewId; } } @@ -850,7 +853,7 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: const adUnitMediaTypes = Object.keys(adUnit.mediaTypes || { 'banner': 'banner' }); // get the bidder's mediaTypes - const allBidders = adUnit.bids.map(bid => bid.bidder); + const allBidders = adUnit.bids.map(bid => bid.bidder).filter(Boolean); const bidderRegistry = adapterManager.bidderRegistry; const bidders = allBidders.filter(bidder => !s2sBidders.has(bidder)); @@ -1168,6 +1171,14 @@ addApiMethod('setBidderConfig', config.setBidderConfig); pbjsInstance.que.push(() => listenMessagesFromCreative()); +let queSetupComplete; + +export function resetQueSetup() { + queSetupComplete = defer(); +} + +resetQueSetup(); + /** * This queue lets users load Prebid asynchronously, but run functions the same way regardless of whether it gets loaded * before or after their script executes. For example, given the code: @@ -1189,15 +1200,17 @@ pbjsInstance.que.push(() => listenMessagesFromCreative()); * @alias module:pbjs.que.push */ function quePush(command) { - if (typeof command === 'function') { - try { - command.call(); - } catch (e) { - logError('Error processing command :', e.message, e.stack); + queSetupComplete.promise.then(() => { + if (typeof command === 'function') { + try { + command.call(); + } catch (e) { + logError('Error processing command :', e.message, e.stack); + } + } else { + logError(`Commands written into ${getGlobalVarName()}.cmd.push must be wrapped in a function`); } - } else { - logError(`Commands written into ${getGlobalVarName()}.cmd.push must be wrapped in a function`); - } + }) } async function _processQueue(queue) { @@ -1223,8 +1236,12 @@ const processQueue = delayIfPrerendering(() => pbjsInstance.delayPrerendering, a pbjsInstance.que.push = pbjsInstance.cmd.push = quePush; insertLocatorFrame(); hook.ready(); - await _processQueue(pbjsInstance.que); - await _processQueue(pbjsInstance.cmd); + try { + await _processQueue(pbjsInstance.que); + await _processQueue(pbjsInstance.cmd); + } finally { + queSetupComplete.resolve(); + } }) addApiMethod('processQueue', processQueue, false); @@ -1245,4 +1262,15 @@ function triggerBilling({adId, adUnitCode}: { } addApiMethod('triggerBilling', triggerBilling); +/** + * Refreshes the previously generated page view ID. Can be used to instruct bidders + * that use page view ID to consider future auctions as part of a new page load. + */ +function refreshPageViewId() { + for (const key of pbjsInstance.pageViewIdPerBidder.keys()) { + pbjsInstance.pageViewIdPerBidder.set(key, generateUUID()); + } +} +addApiMethod('refreshPageViewId', refreshPageViewId); + export default pbjsInstance; diff --git a/src/targeting.ts b/src/targeting.ts index 4016c5b4bd3..477ddaab7f0 100644 --- a/src/targeting.ts +++ b/src/targeting.ts @@ -73,9 +73,10 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, const bidsByBidder = groupBy(buckets[bucketKey], 'bidderCode') Object.keys(bidsByBidder).forEach(key => { bucketBids.push(bidsByBidder[key].reduce(winReducer)) }); // if adUnitBidLimit is set, pass top N number bids - if (adUnitBidLimit) { + const bidLimit = typeof adUnitBidLimit === 'object' ? adUnitBidLimit[bucketKey] : adUnitBidLimit; + if (bidLimit) { bucketBids = dealPrioritization ? bucketBids.sort(sortByDealAndPriceBucketOrCpm(true)) : bucketBids.sort((a, b) => b.cpm - a.cpm); - bids.push(...bucketBids.slice(0, adUnitBidLimit)); + bids.push(...bucketBids.slice(0, bidLimit)); } else { bucketBids = bucketBids.sort(winSorter) bids.push(...bucketBids); @@ -142,6 +143,30 @@ export function getGPTSlotsForAdUnits(adUnitCodes: AdUnitCode[], customSlotMatch return auToSlots; }, Object.fromEntries(adUnitCodes.map(au => [au, []]))); } +/* * + * Returns a map of adUnitCodes to their bid limits. If sendAllBids is disabled, all adUnits will have a bid limit of 0. + * If sendAllBids is enabled, the bid limit for each adUnit will be determined by the following precedence: + * 1. The bidLimit property of the adUnit object + * 2. The bidLimit parameter passed to this function + * 3. The global sendBidsControl.bidLimit config property + * + * @param adUnitCodes + * @param bidLimit + */ +export function getAdUnitBidLimitMap(adUnitCodes: AdUnitCode[], bidLimit: number): ByAdUnit | number { + if (!config.getConfig('enableSendAllBids')) return 0; + const bidLimitConfigValue = config.getConfig('sendBidsControl.bidLimit'); + const adUnitCodesSet = new Set(adUnitCodes); + + const result: ByAdUnit = {}; + for (const au of auctionManager.getAdUnits()) { + if (adUnitCodesSet.has(au.code)) { + result[au.code] = au?.bidLimit || bidLimit || bidLimitConfigValue; + } + } + + return result; +} export type TargetingMap = Partial & { [targetingKey: string]: V @@ -238,9 +263,7 @@ export function newTargeting(auctionManager) { getAllTargeting(adUnitCode?: AdUnitCode | AdUnitCode[], bidLimit?: number, bidsReceived?: Bid[], winReducer = getHighestCpm, winSorter = sortByHighestCpm): ByAdUnit { bidsReceived ||= getBidsReceived(winReducer, winSorter); const adUnitCodes = getAdUnitCodes(adUnitCode); - const sendAllBids = config.getConfig('enableSendAllBids'); - const bidLimitConfigValue = config.getConfig('sendBidsControl.bidLimit'); - const adUnitBidLimit = (sendAllBids && (bidLimit || bidLimitConfigValue)) || 0; + const adUnitBidLimit = getAdUnitBidLimitMap(adUnitCodes, bidLimit); const { customKeysByUnit, filteredBids } = getfilteredBidsAndCustomKeys(adUnitCodes, bidsReceived); const bidsSorted = getHighestCpmBidsFromBidPool(filteredBids, winReducer, adUnitBidLimit, undefined, winSorter); let targeting = getTargetingLevels(bidsSorted, customKeysByUnit, adUnitCodes); diff --git a/src/types/common.d.ts b/src/types/common.d.ts index b636a6cbabe..3f385ab6868 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -14,6 +14,12 @@ export type Currency = string; export type AdUnitCode = string; export type Size = [number, number]; export type ContextIdentifiers = { + /** + * Page view ID. Unique for a page view (one load of Prebid); can also be refreshed programmatically. + * Shared across all requests and responses within the page view, for the same bidder. + * Different bidders see a different page view ID. + */ + pageViewId: Identifier; /** * Auction ID. Unique for any given auction, but shared across all requests and responses within that auction. */ diff --git a/src/types/ortb/request.d.ts b/src/types/ortb/request.d.ts index 320e13965ed..64cf842ccb1 100644 --- a/src/types/ortb/request.d.ts +++ b/src/types/ortb/request.d.ts @@ -5,9 +5,16 @@ import type {DSARequest} from "./ext/dsa.d.ts"; import type {BidRequest, Imp} from 'iab-openrtb/v26'; +type TidSource = 'pbjs' | 'pbjsStable' | 'pub'; + export interface ORTBRequest extends BidRequest { + source: BidRequest['source'] & { + ext: Ext & { + tidSource: TidSource + } + } ext: Ext & { - dsa?: DSARequest + dsa?: DSARequest; } } @@ -30,5 +37,12 @@ export type ORTBImp = Imp & { * but common across all requests for that opportunity. */ tid?: string; + /** + * Indicates which entity generated the TID. + * - pbjs: Prebid generated the TID and it is bidder-specific. + * - pbjsStable: Prebid generated the TID and it is consistent across bidders. + * - pub: The publisher supplied the TID. + */ + tidSource?: TidSource; } }; diff --git a/src/utils/cachedApiWrapper.js b/src/utils/cachedApiWrapper.js new file mode 100644 index 00000000000..9939f23df17 --- /dev/null +++ b/src/utils/cachedApiWrapper.js @@ -0,0 +1,26 @@ +export function CachedApiWrapper(getTarget, props) { + const wrapper = {}; + let data = {}; + const children = []; + Object.entries(props).forEach(([key, value]) => { + if (value != null && typeof value === 'object') { + const child = new CachedApiWrapper(() => getTarget()?.[key], value) + wrapper[key] = child.obj; + children.push(child.reset); + } else if (value === true) { + Object.defineProperty(wrapper, key, { + get() { + if (!data.hasOwnProperty(key)) { + data[key] = getTarget()?.[key]; + } + return data[key]; + } + }) + } + }) + this.obj = wrapper; + this.reset = function () { + children.forEach(reset => reset()); + data = {}; + }; +} diff --git a/src/utils/winDimensions.js b/src/utils/winDimensions.js index 2773ffc1721..01122a9761c 100644 --- a/src/utils/winDimensions.js +++ b/src/utils/winDimensions.js @@ -1,65 +1,55 @@ import {canAccessWindowTop, internal as utilsInternals} from '../utils.js'; +import {CachedApiWrapper} from './cachedApiWrapper.js'; const CHECK_INTERVAL_MS = 20; -export function cachedGetter(getter) { - let value, lastCheckTimestamp; - return { - get: function () { - if (!value || !lastCheckTimestamp || (Date.now() - lastCheckTimestamp > CHECK_INTERVAL_MS)) { - value = getter(); - lastCheckTimestamp = Date.now(); - } - return value; - }, - reset: function () { - value = getter(); - } - } -} - -function fetchWinDimensions() { - const top = canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf(); - - return { +const winDimensions = new CachedApiWrapper( + () => canAccessWindowTop() ? utilsInternals.getWindowTop() : utilsInternals.getWindowSelf(), + { + innerHeight: true, + innerWidth: true, screen: { - width: top.screen?.width, - height: top.screen?.height + width: true, + height: true, }, - innerHeight: top.innerHeight, - innerWidth: top.innerWidth, visualViewport: { - height: top.visualViewport?.height, - width: top.visualViewport?.width, + width: true, + height: true }, document: { documentElement: { - clientWidth: top.document?.documentElement?.clientWidth, - clientHeight: top.document?.documentElement?.clientHeight, - scrollTop: top.document?.documentElement?.scrollTop, - scrollLeft: top.document?.documentElement?.scrollLeft, + clientWidth: true, + clientHeight: true, + scrollTop: true, + scrollLeft: true }, body: { - scrollTop: document.body?.scrollTop, - scrollLeft: document.body?.scrollLeft, - clientWidth: document.body?.clientWidth, - clientHeight: document.body?.clientHeight, - }, + scrollTop: true, + scrollLeft: true, + clientWidth: true, + clientHeight: true + } } - }; -} + } +); + export const internal = { - fetchWinDimensions, - resetters: [] + reset: winDimensions.reset, }; -const winDimensions = cachedGetter(() => internal.fetchWinDimensions()); - -export const getWinDimensions = winDimensions.get; -internal.resetters.push(winDimensions.reset); +export const getWinDimensions = (() => { + let lastCheckTimestamp; + return function () { + if (!lastCheckTimestamp || (Date.now() - lastCheckTimestamp > CHECK_INTERVAL_MS)) { + internal.reset(); + lastCheckTimestamp = Date.now(); + } + return winDimensions.obj; + } +})(); export function resetWinDimensions() { - internal.resetters.forEach(fn => fn()); + internal.reset(); } export function getScreenOrientation(win) { diff --git a/test/spec/activities/objectGuard_spec.js b/test/spec/activities/objectGuard_spec.js index c88442e9111..5a86160c1f3 100644 --- a/test/spec/activities/objectGuard_spec.js +++ b/test/spec/activities/objectGuard_spec.js @@ -1,4 +1,5 @@ import {objectGuard, writeProtectRule} from '../../../libraries/objectGuard/objectGuard.js'; +import {redactRule} from '../../../src/activities/redactor.js'; describe('objectGuard', () => { describe('read rule', () => { @@ -12,8 +13,13 @@ describe('objectGuard', () => { get(val) { return `repl${val}` }, } }) + it('should preserve object identity', () => { + const guard = objectGuard([rule])({outer: {inner: {foo: 'bar'}}}); + expect(guard.outer).to.equal(guard.outer); + expect(guard.outer.inner).to.equal(guard.outer.inner); + }) it('can prevent top level read access', () => { - const {obj} = objectGuard([rule])({'foo': 1, 'other': 2}); + const obj = objectGuard([rule])({'foo': 1, 'other': 2}); expect(obj).to.eql({ foo: 'repl1', other: 2 @@ -21,20 +27,27 @@ describe('objectGuard', () => { }); it('does not choke if a guarded property is missing', () => { - const {obj} = objectGuard([rule])({}); + const obj = objectGuard([rule])({}); expect(obj.foo).to.not.exist; }); + it('allows concurrent reads', () => { + const obj = {'foo': 'bar'}; + const guarded = objectGuard([rule])(obj); + obj.foo = 'baz'; + expect(guarded.foo).to.eql('replbaz'); + }) + it('does not prevent access if applies returns false', () => { applies = false; - const {obj} = objectGuard([rule])({foo: 1}); + const obj = objectGuard([rule])({foo: 1}); expect(obj).to.eql({ foo: 1 }); }) it('can prevent nested property access', () => { - const {obj} = objectGuard([rule])({ + const obj = objectGuard([rule])({ other: 0, outer: { foo: 1, @@ -60,6 +73,11 @@ describe('objectGuard', () => { }) }); + it('prevents nested property access when a parent property is protected', () => { + const guard = objectGuard([rule])({foo: {inner: 'value'}}); + expect(guard.inner?.value).to.not.exist; + }) + it('does not call applies more than once', () => { JSON.stringify(objectGuard([rule])({ foo: 0, @@ -68,7 +86,7 @@ describe('objectGuard', () => { foo: 1 } } - }).obj); + })); expect(rule.applies.callCount).to.equal(1); }) }); @@ -84,35 +102,66 @@ describe('objectGuard', () => { }); }); - it('should undo top-level writes', () => { + it('should preserve object identity', () => { + const guard = objectGuard([rule])({outer: {inner: {foo: 'bar'}}}); + expect(guard.outer).to.equal(guard.outer); + expect(guard.outer.inner).to.equal(guard.outer.inner); + }) + + it('does not mess up array reads', () => { + const guard = objectGuard([rule])({foo: [{bar: 'baz'}]}); + expect(guard.foo).to.eql([{bar: 'baz'}]); + }) + + it('prevents array modification', () => { + const obj = {foo: ['value']}; + const guard = objectGuard([rule])(obj); + guard.foo.pop(); + guard.foo.push('test'); + expect(obj.foo).to.eql(['value']); + }) + + it('allows array modification when not applicable', () => { + applies = false; + const obj = {foo: ['value']}; + const guard = objectGuard([rule])(obj); + guard.foo.pop(); + guard.foo.push('test'); + expect(obj.foo).to.eql(['test']); + }) + + it('should prevent top-level writes', () => { const obj = {bar: {nested: 'val'}, other: 'val'}; const guard = objectGuard([rule])(obj); - guard.obj.foo = 'denied'; - guard.obj.bar.nested = 'denied'; - guard.obj.bar.other = 'denied'; - guard.obj.other = 'allowed'; - guard.verify(); - expect(obj).to.eql({bar: {nested: 'val'}, other: 'allowed'}); + guard.foo = 'denied'; + guard.bar.nested = 'denied'; + guard.bar.other = 'denied'; + guard.other = 'allowed'; + expect(guard).to.eql({bar: {nested: 'val'}, other: 'allowed'}); }); - it('should undo top-level deletes', () => { + it('should not prevent no-op writes', () => { + const guard = objectGuard([rule])({foo: {some: 'value'}}); + guard.foo = {some: 'value'}; + sinon.assert.notCalled(rule.applies); + }) + + it('should prevent top-level deletes', () => { const obj = {foo: {nested: 'val'}, bar: 'val'}; const guard = objectGuard([rule])(obj); - delete guard.obj.foo.nested; - delete guard.obj.bar; - guard.verify(); - expect(obj).to.eql({foo: {nested: 'val'}, bar: 'val'}); + delete guard.foo.nested; + delete guard.bar; + expect(guard).to.eql({foo: {nested: 'val'}, bar: 'val'}); }) - it('should undo nested writes', () => { + it('should prevent nested writes', () => { const obj = {outer: {inner: {bar: {nested: 'val'}, other: 'val'}}}; const guard = objectGuard([rule])(obj); - guard.obj.outer.inner.bar.other = 'denied'; - guard.obj.outer.inner.bar.nested = 'denied'; - guard.obj.outer.inner.foo = 'denied'; - guard.obj.outer.inner.other = 'allowed'; - guard.verify(); - expect(obj).to.eql({ + guard.outer.inner.bar.other = 'denied'; + guard.outer.inner.bar.nested = 'denied'; + guard.outer.inner.foo = 'denied'; + guard.outer.inner.other = 'allowed'; + expect(guard).to.eql({ outer: { inner: { bar: { @@ -124,21 +173,107 @@ describe('objectGuard', () => { }) }); - it('should undo nested deletes', () => { + it('should prevent writes if upper levels are protected', () => { + const obj = {foo: {inner: {}}}; + const guard = objectGuard([rule])(obj); + guard.foo.inner.prop = 'value'; + expect(obj).to.eql({foo: {inner: {}}}); + }) + + it('should prevent deletes if a higher level property is protected', () => { + const obj = {foo: {inner: {prop: 'value'}}}; + const guard = objectGuard([rule])(obj); + delete guard.foo.inner.prop; + expect(obj).to.eql({foo: {inner: {prop: 'value'}}}); + }) + + it('should clean up top-level writes that would result in inner properties changing', () => { + const guard = objectGuard([rule])({outer: {inner: {bar: 'baz'}}}); + guard.outer = {inner: {bar: 'baz', foo: 'baz', prop: 'allowed'}}; + expect(guard).to.eql({outer: {inner: {bar: 'baz', prop: 'allowed'}}}); + }) + + it('should not prevent writes that are not protected', () => { + const obj = {}; + const guard = objectGuard([rule])(obj); + guard.outer = { + test: 'value' + } + expect(obj.outer.test).to.eql('value'); + }) + + it('should not choke on type mismatch: overwrite object with scalar', () => { + const obj = {outer: {inner: {}}}; + const guard = objectGuard([rule])(obj); + guard.outer = null; + expect(obj).to.eql({outer: {inner: {}}}); + }); + + it('should not choke on type mismatch: overwrite scalar with object', () => { + const obj = {outer: null}; + const guard = objectGuard([rule])(obj); + guard.outer = {inner: {bar: 'denied', other: 'allowed'}}; + expect(obj).to.eql({outer: {inner: {other: 'allowed'}}}); + }) + + it('should prevent nested deletes', () => { const obj = {outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}; const guard = objectGuard([rule])(obj); - delete guard.obj.outer.inner.foo.nested; - delete guard.obj.outer.inner.bar; - guard.verify(); - expect(obj).to.eql({outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}) + delete guard.outer.inner.foo.nested; + delete guard.outer.inner.bar; + expect(guard).to.eql({outer: {inner: {foo: {nested: 'val'}, bar: 'val'}}}) }); + it('should prevent higher level deletes that would result in inner properties changing', () => { + const guard = objectGuard([rule])({outer: {inner: {bar: 'baz'}}}); + delete guard.outer.inner; + expect(guard).to.eql({outer: {inner: {bar: 'baz'}}}); + }) + it('should work on null properties', () => { const obj = {foo: null}; const guard = objectGuard([rule])(obj); - guard.obj.foo = 'denied'; - guard.verify(); - expect(obj).to.eql({foo: null}); + guard.foo = 'denied'; + expect(guard).to.eql({foo: null}); }); }); + describe('multiple rules on the same path', () => { + it('should each be checked for redacts', () => { + const obj = objectGuard([ + redactRule({ + paths: ['foo'], + applies: () => true, + get(val) { + return '1' + val; + } + }), + redactRule({ + paths: ['foo'], + applies: () => true, + get(val) { + return '2' + val; + } + }) + ])({foo: 'bar'}); + expect(obj.foo).to.eql('21bar'); + }); + + it('can apply both redact and write protect', () => { + const obj = objectGuard([ + redactRule({ + paths: ['foo'], + applies: () => true, + get(val) { + return 'redact' + val; + }, + }), + writeProtectRule({ + paths: ['foo'], + applies: () => true, + }) + ])({foo: 'bar'}); + obj.foo = 'baz'; + expect(obj.foo).to.eql('redactbar'); + }); + }) }); diff --git a/test/spec/activities/ortbGuard_spec.js b/test/spec/activities/ortbGuard_spec.js index 828cbe4e328..1384a6c0674 100644 --- a/test/spec/activities/ortbGuard_spec.js +++ b/test/spec/activities/ortbGuard_spec.js @@ -45,7 +45,7 @@ describe('ortb2Guard', () => { function testPropertiesAreProtected(properties, allowed) { properties.forEach(prop => { - it(`should ${allowed ? 'keep' : 'undo'} additions to ${prop}`, () => { + it(`should ${allowed ? 'keep' : 'prevent'} additions to ${prop}`, () => { const orig = [{n: 'orig'}]; const ortb2 = {}; deepSetValue(ortb2, prop, deepClone(orig)); @@ -53,8 +53,7 @@ describe('ortb2Guard', () => { const mod = {}; const insert = [{n: 'new'}]; deepSetValue(mod, prop, insert); - mergeDeep(guard.obj, mod); - guard.verify(); + mergeDeep(guard, mod); const actual = deepAccess(ortb2, prop); if (allowed) { expect(actual).to.eql(orig.concat(insert)) @@ -63,13 +62,12 @@ describe('ortb2Guard', () => { } }); - it(`should ${allowed ? 'keep' : 'undo'} modifications to ${prop}`, () => { + it(`should ${allowed ? 'keep' : 'prevent'} modifications to ${prop}`, () => { const orig = [{n: 'orig'}]; const ortb2 = {}; deepSetValue(ortb2, prop, orig); const guard = ortb2Guard(ortb2, activityParams(MOD_TYPE, MOD_NAME)); - deepSetValue(guard.obj, `${prop}.0.n`, 'new'); - guard.verify(); + deepSetValue(guard, `${prop}.0.n`, 'new'); const actual = deepAccess(ortb2, prop); if (allowed) { expect(actual).to.eql([{n: 'new'}]); @@ -102,19 +100,18 @@ describe('ortb2FragmentsGuard', () => { guardFragments = ortb2FragmentsGuardFactory(testGuard); }); - it('should undo changes to global FPD', () => { + it('should prevent changes to global FPD', () => { const fragments = { global: { foo: {inner: 'val'} } } const guard = guardFragments(fragments); - guard.obj.global.foo = 'other'; - guard.verify(); + guard.global.foo = 'other'; expect(fragments.global.foo).to.eql({inner: 'val'}); }); - it('should undo changes to bidder FPD', () => { + it('should prevent changes to bidder FPD', () => { const fragments = { bidder: { A: { @@ -123,18 +120,16 @@ describe('ortb2FragmentsGuard', () => { } }; const guard = guardFragments(fragments); - guard.obj.bidder.A.foo = 'denied'; - guard.verify(); + guard.bidder.A.foo = 'denied'; expect(fragments.bidder.A).to.eql({foo: 'val'}); }); - it('should undo changes to bidder FPD that was not initially there', () => { + it('should prevent changes to bidder FPD that was not initially there', () => { const fragments = { bidder: {} }; const guard = guardFragments(fragments); - guard.obj.bidder.A = {foo: 'denied', other: 'allowed'}; - guard.verify(); + guard.bidder.A = {foo: 'denied', other: 'allowed'}; expect(fragments.bidder.A).to.eql({other: 'allowed'}); }); }) diff --git a/test/spec/activities/redactor_spec.js b/test/spec/activities/redactor_spec.js index c5c194da73d..e1d565d6d60 100644 --- a/test/spec/activities/redactor_spec.js +++ b/test/spec/activities/redactor_spec.js @@ -276,7 +276,7 @@ describe('redactor', () => { }); testAllowDeny(ACTIVITY_TRANSMIT_TID, (allowed) => { - testPropertiesAreRemoved(() => redactor.bidRequest, ['ortb2Imp.ext.tid'], allowed); + testPropertiesAreRemoved(() => redactor.bidRequest, ['ortb2Imp.ext.tid', 'ortb2Imp.ext.tidSource'], allowed); }) }); @@ -290,7 +290,7 @@ describe('redactor', () => { }); testAllowDeny(ACTIVITY_TRANSMIT_TID, (allowed) => { - testPropertiesAreRemoved(() => redactor.ortb2, ['source.tid'], allowed); + testPropertiesAreRemoved(() => redactor.ortb2, ['source.tid', 'source.ext.tidSource'], allowed); }); testAllowDeny(ACTIVITY_TRANSMIT_PRECISE_GEO, (allowed) => { diff --git a/test/spec/banner_spec.js b/test/spec/banner_spec.js index fcf56ad6e10..f60f2023194 100644 --- a/test/spec/banner_spec.js +++ b/test/spec/banner_spec.js @@ -127,6 +127,23 @@ describe('banner', () => { assert.ok(logWarnSpy.calledOnce, 'expected warning was logged due to conflicting btype'); }); + it('should not warn if fields match', () => { + const adUnit = { + mediaTypes: { + banner: { + format: [{wratio: 1, hratio: 1}] + } + }, + ortb2Imp: { + banner: { + format: [{wratio: 1, hratio: 1}] + } + } + } + syncOrtb2(adUnit, 'banner'); + sinon.assert.notCalled(logWarnSpy); + }) + it('should omit sync if mediaType not present on adUnit', () => { const adUnit = { mediaTypes: { diff --git a/test/spec/libraries/cmUtils_spec.js b/test/spec/libraries/cmUtils_spec.js index 7f9c2c932b3..be2e31a8e18 100644 --- a/test/spec/libraries/cmUtils_spec.js +++ b/test/spec/libraries/cmUtils_spec.js @@ -1,5 +1,5 @@ import * as utils from 'src/utils.js'; -import {lookupConsentData, consentManagementHook} from '../../../libraries/consentManagement/cmUtils.js'; +import {lookupConsentData, consentManagementHook, configParser} from '../../../libraries/consentManagement/cmUtils.js'; describe('consent management utils', () => { let sandbox, clock; @@ -226,4 +226,126 @@ describe('consent management utils', () => { }) }); }); + + describe('configParser', () => { + let namespace, displayName, consentDataHandler, parseConsentData, getNullConsent, cmpHandlers, cmpEventCleanup; + let getConsentConfig, resetConsentDataHandler; + + beforeEach(() => { + namespace = 'test'; + displayName = 'TEST'; + resetConsentDataHandler = sinon.stub(); + consentDataHandler = { + reset: sinon.stub(), + removeCmpEventListener: sinon.stub(), + getConsentData: sinon.stub(), + setConsentData: sinon.stub() + }; + parseConsentData = sinon.stub().callsFake(data => data); + getNullConsent = sinon.stub().returns({consent: null}); + cmpHandlers = { + iab: sinon.stub().returns(Promise.resolve()) + }; + cmpEventCleanup = sinon.stub(); + + // Create a spy for resetConsentDataHandler to verify it's called + const configParserInstance = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers, + cmpEventCleanup + }); + + getConsentConfig = configParserInstance; + }); + + it('should reset and return empty object when config is not defined', () => { + const result = getConsentConfig(); + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config not defined')); + }); + + it('should reset and return empty object when config is not an object', () => { + const result = getConsentConfig({[namespace]: 'not an object'}); + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config not defined')); + }); + + describe('when module is explicitly disabled', () => { + it('should reset consent data handler and return empty object when enabled is false', () => { + const result = getConsentConfig({[namespace]: {enabled: false}}); + + expect(result).to.deep.equal({}); + sinon.assert.calledWith(utils.logWarn, sinon.match('config enabled is set to false')); + }); + + it('should call cmpEventCleanup when enabled is false', () => { + getConsentConfig({[namespace]: {enabled: false}}); + + sinon.assert.called(cmpEventCleanup); + sinon.assert.calledWith(utils.logWarn, sinon.match('config enabled is set to false')); + }); + + it('should handle cmpEventCleanup errors gracefully', () => { + const cleanupError = new Error('Cleanup failed'); + cmpEventCleanup.throws(cleanupError); + + getConsentConfig({[namespace]: {enabled: false}}); + + sinon.assert.called(cmpEventCleanup); + sinon.assert.calledWith(utils.logError, sinon.match('Error during CMP event cleanup'), cleanupError); + }); + + it('should not call cmpEventCleanup when enabled is true', () => { + getConsentConfig({[namespace]: {enabled: true, cmpApi: 'iab'}}); + + sinon.assert.notCalled(cmpEventCleanup); + }); + + it('should not call cmpEventCleanup when enabled is not specified', () => { + getConsentConfig({[namespace]: {cmpApi: 'iab'}}); + + sinon.assert.notCalled(cmpEventCleanup); + }); + }); + + describe('cmpEventCleanup parameter', () => { + it('should work without cmpEventCleanup parameter', () => { + const configParserWithoutCleanup = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers + // No cmpEventCleanup provided + }); + + const result = configParserWithoutCleanup({[namespace]: {enabled: false}}); + expect(result).to.deep.equal({}); + // Should not throw error when cmpEventCleanup is undefined + }); + + it('should only call cmpEventCleanup if it is a function', () => { + const configParserWithNonFunction = configParser({ + namespace, + displayName, + consentDataHandler, + parseConsentData, + getNullConsent, + cmpHandlers, + cmpEventCleanup: 'not a function' + }); + + const result = configParserWithNonFunction({[namespace]: {enabled: false}}); + expect(result).to.deep.equal({}); + // Should not throw error when cmpEventCleanup is not a function + }); + }); + + // Additional tests for other configParser functionality could be added here + }); }); diff --git a/test/spec/libraries/cmp/cmpEventUtils_spec.js b/test/spec/libraries/cmp/cmpEventUtils_spec.js new file mode 100644 index 00000000000..c3262549b20 --- /dev/null +++ b/test/spec/libraries/cmp/cmpEventUtils_spec.js @@ -0,0 +1,330 @@ +import * as utils from 'src/utils.js'; +import { + BaseCmpEventManager, + TcfCmpEventManager, + GppCmpEventManager, + createCmpEventManager +} from '../../../../libraries/cmp/cmpEventUtils.js'; + +describe('CMP Event Utils', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + ['logError', 'logInfo', 'logWarn'].forEach(n => sandbox.stub(utils, n)); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('BaseCmpEventManager', () => { + let manager; + + // Create a concrete implementation for testing the abstract class + class TestCmpEventManager extends BaseCmpEventManager { + removeCmpEventListener() { + const params = this.getRemoveListenerParams(); + if (params) { + this.getCmpApi()(params); + } + } + } + + beforeEach(() => { + manager = new TestCmpEventManager(); + }); + + describe('setCmpApi and getCmpApi', () => { + it('should set and get CMP API', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + expect(manager.getCmpApi()).to.equal(mockCmpApi); + }); + + it('should initialize with null CMP API', () => { + expect(manager.getCmpApi()).to.be.null; + }); + }); + + describe('setCmpListenerId and getCmpListenerId', () => { + it('should set and get listener ID', () => { + manager.setCmpListenerId(123); + expect(manager.getCmpListenerId()).to.equal(123); + }); + + it('should handle undefined listener ID', () => { + manager.setCmpListenerId(undefined); + expect(manager.getCmpListenerId()).to.be.undefined; + }); + + it('should handle zero as valid listener ID', () => { + manager.setCmpListenerId(0); + expect(manager.getCmpListenerId()).to.equal(0); + }); + + it('should initialize with undefined listener ID', () => { + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('resetCmpApis', () => { + it('should reset both CMP API and listener ID', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(456); + + manager.resetCmpApis(); + + expect(manager.getCmpApi()).to.be.null; + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('getRemoveListenerParams', () => { + it('should return params when CMP API and listener ID are valid', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + + expect(params).to.not.be.null; + expect(params.command).to.equal('removeEventListener'); + expect(params.parameter).to.equal(123); + expect(params.callback).to.be.a('function'); + }); + + it('should return null when CMP API is null', () => { + manager.setCmpApi(null); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when CMP API is not a function', () => { + manager.setCmpApi('not a function'); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when listener ID is undefined', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(undefined); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return null when listener ID is null', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(null); + + const params = manager.getRemoveListenerParams(); + expect(params).to.be.null; + }); + + it('should return params when listener ID is 0', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(0); + + const params = manager.getRemoveListenerParams(); + expect(params).to.not.be.null; + expect(params.parameter).to.equal(0); + }); + + it('should call resetCmpApis when callback is executed', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + const params = manager.getRemoveListenerParams(); + params.callback(); + + expect(manager.getCmpApi()).to.be.null; + expect(manager.getCmpListenerId()).to.be.undefined; + }); + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with params when conditions are met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(123); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(123); + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + }); + }); + }); + + describe('TcfCmpEventManager', () => { + let manager, mockGetConsentData; + + beforeEach(() => { + mockGetConsentData = sinon.stub(); + manager = new TcfCmpEventManager(mockGetConsentData); + }); + + it('should initialize with provided getConsentData function', () => { + expect(manager.getConsentData).to.equal(mockGetConsentData); + }); + + it('should initialize with default getConsentData when not provided', () => { + const defaultManager = new TcfCmpEventManager(); + expect(defaultManager.getConsentData()).to.be.null; + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with TCF-specific params including apiVersion', () => { + const mockCmpApi = sinon.stub(); + const consentData = { apiVersion: 2 }; + mockGetConsentData.returns(consentData); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(456); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + sinon.assert.calledWith(utils.logInfo, 'Removing TCF CMP event listener'); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(456); + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should use default apiVersion when consent data has no apiVersion', () => { + const mockCmpApi = sinon.stub(); + const consentData = {}; // No apiVersion + mockGetConsentData.returns(consentData); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(789); + + manager.removeCmpEventListener(); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should use default apiVersion when consent data is null', () => { + const mockCmpApi = sinon.stub(); + mockGetConsentData.returns(null); + + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(789); + + manager.removeCmpEventListener(); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.apiVersion).to.equal(2); + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + sinon.assert.notCalled(utils.logInfo); + }); + }); + }); + + describe('GppCmpEventManager', () => { + let manager; + + beforeEach(() => { + manager = new GppCmpEventManager(); + }); + + describe('removeCmpEventListener', () => { + it('should call CMP API with GPP-specific params', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + manager.setCmpListenerId(321); + + manager.removeCmpEventListener(); + + sinon.assert.calledOnce(mockCmpApi); + sinon.assert.calledWith(utils.logInfo, 'Removing GPP CMP event listener'); + + const callArgs = mockCmpApi.getCall(0).args[0]; + expect(callArgs.command).to.equal('removeEventListener'); + expect(callArgs.parameter).to.equal(321); + expect(callArgs.apiVersion).to.be.undefined; // GPP doesn't set apiVersion + }); + + it('should not call CMP API when conditions are not met', () => { + const mockCmpApi = sinon.stub(); + manager.setCmpApi(mockCmpApi); + // No listener ID set + + manager.removeCmpEventListener(); + + sinon.assert.notCalled(mockCmpApi); + sinon.assert.notCalled(utils.logInfo); + }); + }); + }); + + describe('createCmpEventManager', () => { + it('should create TcfCmpEventManager for tcf type', () => { + const mockGetConsentData = sinon.stub(); + const manager = createCmpEventManager('tcf', mockGetConsentData); + + expect(manager).to.be.instanceOf(TcfCmpEventManager); + expect(manager.getConsentData).to.equal(mockGetConsentData); + }); + + it('should create TcfCmpEventManager without getConsentData function', () => { + const manager = createCmpEventManager('tcf'); + + expect(manager).to.be.instanceOf(TcfCmpEventManager); + expect(manager.getConsentData()).to.be.null; + }); + + it('should create GppCmpEventManager for gpp type', () => { + const manager = createCmpEventManager('gpp'); + + expect(manager).to.be.instanceOf(GppCmpEventManager); + }); + + it('should log error and return null for unknown type', () => { + const manager = createCmpEventManager('unknown'); + + expect(manager).to.be.null; + sinon.assert.calledWith(utils.logError, 'Unknown CMP type: unknown'); + }); + + it('should ignore getConsentData parameter for gpp type', () => { + const mockGetConsentData = sinon.stub(); + const manager = createCmpEventManager('gpp', mockGetConsentData); + + expect(manager).to.be.instanceOf(GppCmpEventManager); + // GPP manager doesn't use getConsentData + }); + }); +}); diff --git a/test/spec/libraries/dnt_spec.js b/test/spec/libraries/dnt_spec.js new file mode 100644 index 00000000000..177542dca31 --- /dev/null +++ b/test/spec/libraries/dnt_spec.js @@ -0,0 +1,34 @@ +import {getDNT} from '../../../libraries/dnt/index.js'; + +describe('dnt helper', () => { + let win; + beforeEach(() => { + win = {}; + }); + + [ + 'top', + 'doNotTrack', + 'navigator', + 'navigator.doNotTrack', + 'top.doNotTrack', + 'top.navigator.doNotTrack' + ].forEach(path => { + it(`should not choke if ${path} throws`, () => { + path = path.split('.'); + path.reduce((parent, name, i) => { + if (i === path.length - 1) { + Object.defineProperty(parent, name, { + get() { + throw new Error(); + } + }) + } else { + parent = parent[name] = {}; + } + return parent; + }, win); + expect(getDNT(win)).to.be.false; + }) + }) +}) diff --git a/test/spec/libraries/extraWinDimensions_spec.js b/test/spec/libraries/extraWinDimensions_spec.js deleted file mode 100644 index 32c8a35a95f..00000000000 --- a/test/spec/libraries/extraWinDimensions_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import {resetWinDimensions} from '../../../src/utils.js'; -import * as extraWinDimensions from '../../../libraries/extraWinDimensions/extraWinDimensions.js'; - -describe('extraWinDimensions', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.stub() - }) - - it('should reset together with basic dimensions', () => { - const resetSpy = sinon.spy(extraWinDimensions.internal, 'fetchExtraDimensions'); - resetWinDimensions(); - sinon.assert.called(resetSpy); - }) -}); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index 9178d7b532d..d77a0fb8bd6 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -4,6 +4,7 @@ import adagioAnalyticsAdapter, { _internal } from 'modules/adagioAnalyticsAdapte import { EVENTS } from 'src/constants.js'; import { expect } from 'chai'; import { server } from 'test/mocks/xhr.js'; +import { deepClone } from 'src/utils.js'; const adapterManager = require('src/adapterManager').default; const events = require('src/events'); @@ -191,8 +192,10 @@ const BID_CACHED = Object.assign({}, BID_ADAGIO, { latestTargetedAuctionId: BID_ADAGIO.auctionId, }); +const PARAMS_PLCMT = 'placement_from_params'; const PARAMS_ADG = { environment: 'desktop', + placement: PARAMS_PLCMT, }; const ORTB_DATA = { @@ -208,6 +211,13 @@ const ADG_RTD = { } }; +const ORTB2IMP_PLCMT = 'placement_from_ortb2imp'; +const ORTB2IMP_DATA_ADG = { + 'adg_rtd': { + 'placement': ORTB2IMP_PLCMT + } +}; + const AUCTION_INIT_ANOTHER = { 'auctionId': AUCTION_ID, 'timestamp': 1519767010567, @@ -228,6 +238,11 @@ const AUCTION_INIT_ANOTHER = { ] } }, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'sizes': [[640, 480]], 'bids': [ { 'bidder': 'another', @@ -250,14 +265,7 @@ const AUCTION_INIT_ANOTHER = { 'publisherId': '1001' }, }, ], - 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'ortb2Imp': { - 'ext': { - 'data': { - 'placement': 'pave_top', - } - } - }, + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' }, { 'code': '/19968336/footer-bid-tag-1', 'mediaTypes': { @@ -281,7 +289,9 @@ const AUCTION_INIT_ANOTHER = { 'ortb2Imp': { 'ext': { 'data': { - 'placement': 'pave_top', + 'adg_rtd': { + 'placement': 'footer' + } } } }, @@ -303,6 +313,11 @@ const AUCTION_INIT_ANOTHER = { 'sizes': [[640, 480]] } }, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'adUnitCode': '/19968336/header-bid-tag-1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], @@ -363,6 +378,12 @@ const AUCTION_INIT_ANOTHER = { 'sizes': [[640, 480]] } }, + + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'adUnitCode': '/19968336/header-bid-tag-1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], @@ -405,6 +426,11 @@ const AUCTION_INIT_ANOTHER = { 'sizes': [[640, 480]] } }, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'adUnitCode': '/19968336/header-bid-tag-1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], @@ -453,7 +479,12 @@ const AUCTION_INIT_ANOTHER = { 'bidderRequestId': '1be65d7958826a', 'auctionId': AUCTION_ID, 'src': 'client', - 'bidRequestsCount': 1 + 'bidRequestsCount': 1, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, } ], 'timeout': 3000, @@ -514,9 +545,7 @@ const AUCTION_INIT_CACHE = { 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'ortb2Imp': { 'ext': { - 'data': { - 'placement': 'pave_top', - } + 'data': ORTB2IMP_DATA_ADG } }, }, { @@ -539,13 +568,6 @@ const AUCTION_INIT_CACHE = { }, } ], 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', - 'ortb2Imp': { - 'ext': { - 'data': { - 'placement': 'pave_top', - } - } - }, } ], 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], 'bidderRequests': [ { @@ -562,6 +584,11 @@ const AUCTION_INIT_CACHE = { 'sizes': [[640, 480]] } }, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'adUnitCode': '/19968336/header-bid-tag-1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], @@ -621,6 +648,11 @@ const AUCTION_INIT_CACHE = { 'sizes': [[640, 480]] } }, + 'ortb2Imp': { + 'ext': { + 'data': ORTB2IMP_DATA_ADG + } + }, 'adUnitCode': '/19968336/header-bid-tag-1', 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', 'sizes': [[640, 480]], @@ -818,7 +850,7 @@ describe('adagio analytics adapter', () => { expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); expect(search.url_dmn).to.equal(window.location.hostname); expect(search.pgtyp).to.equal('article'); - expect(search.plcmt).to.equal('pave_top'); + expect(search.plcmt).to.equal(ORTB2IMP_PLCMT); expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); expect(search.bdrs).to.equal('adagio,another,anotherWithAlias,nobid'); @@ -869,6 +901,7 @@ describe('adagio analytics adapter', () => { expect(search.auct_id).to.equal(RTD_AUCTION_ID); expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); expect(search.win_bdr).to.equal('another'); + expect(search.plcmt).to.equal(ORTB2IMP_PLCMT); expect(search.win_mt).to.equal('ban'); expect(search.win_ban_sz).to.equal('728x90'); expect(search.win_net_cpm).to.equal('2.052'); @@ -877,6 +910,43 @@ describe('adagio analytics adapter', () => { } }); + it('it fallback on the adUnit.params.placement value if adg_rtd.placement is not set', () => { + const mockAuctionInit = deepClone(MOCK.AUCTION_INIT.another); + for (const adUnit of mockAuctionInit.adUnits) { + delete adUnit.ortb2Imp?.ext?.data.adg_rtd; + } + for (const bidRequest of mockAuctionInit.bidderRequests) { + for (const bid of bidRequest.bids) { + delete bid.ortb2Imp?.ext?.data.adg_rtd; + } + } + + events.emit(EVENTS.AUCTION_INIT, mockAuctionInit); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.s_id).to.equal(SESSION_ID); + expect(search.auct_id).to.equal(RTD_AUCTION_ID); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal(PARAMS_PLCMT); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,anotherWithAlias,nobid'); + expect(search.bdrs_code).to.equal('adagio,another,another,nobid'); + expect(search.bdrs_timeout).to.not.exist; + expect(search.adg_mts).to.equal('ban'); + } + }); + it('builds and sends auction data with a cached bid win', () => { events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); events.emit(EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); @@ -903,7 +973,7 @@ describe('adagio analytics adapter', () => { expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); expect(search.url_dmn).to.equal(window.location.hostname); expect(search.pgtyp).to.equal('article'); - expect(search.plcmt).to.equal('pave_top'); + expect(search.plcmt).to.equal(ORTB2IMP_PLCMT); expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); expect(search.bdrs).to.equal('adagio,another'); @@ -928,7 +998,7 @@ describe('adagio analytics adapter', () => { expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); expect(search.url_dmn).to.equal(window.location.hostname); expect(search.pgtyp).to.equal('article'); - expect(search.plcmt).to.equal('pave_top'); + expect(search.plcmt).to.be.undefined; // no placement set, no adagio bidder for this adUnit. expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x480'); expect(search.bdrs).to.equal('another'); @@ -951,7 +1021,7 @@ describe('adagio analytics adapter', () => { expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); expect(search.url_dmn).to.equal(window.location.hostname); expect(search.pgtyp).to.equal('article'); - expect(search.plcmt).to.equal('pave_top'); + expect(search.plcmt).to.equal(ORTB2IMP_PLCMT); expect(search.mts).to.equal('ban'); expect(search.ban_szs).to.equal('640x100,640x480'); expect(search.bdrs).to.equal('adagio,another,anotherWithAlias,nobid'); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 1a98b69c7c8..45788fe14a6 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -283,11 +283,11 @@ describe('Adagio bid adapter', () => { const bidderRequest = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest); + const expectedUrl = `${ENDPOINT}?orgid=1000`; expect(requests).to.have.lengthOf(1); expect(requests[0].method).to.equal('POST'); - expect(requests[0].url).to.equal(ENDPOINT); - expect(requests[0].options.contentType).to.eq('text/plain'); + expect(requests[0].url).to.equal(expectedUrl); expect(requests[0].data).to.have.all.keys(expectedDataKeys); }); @@ -1137,6 +1137,16 @@ describe('Adagio bid adapter', () => { }); }) }) + + describe('with endpoint compression', function() { + it('should always use the endpoint compression option', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].options).to.exist; + expect(requests[0].options.endpointCompression).to.equal(true); + }); + }); }); describe('interpretResponse()', function() { diff --git a/test/spec/modules/adagioRtdProvider_spec.js b/test/spec/modules/adagioRtdProvider_spec.js index 84a726d2cb2..3ac2d1fa73f 100644 --- a/test/spec/modules/adagioRtdProvider_spec.js +++ b/test/spec/modules/adagioRtdProvider_spec.js @@ -507,7 +507,7 @@ describe('Adagio Rtd Provider', function () { expect(ortb2ImpExt.adunit_position).equal(''); }); - describe('update the ortb2Imp.ext.data.placement if not present', function() { + describe('set the ortb2Imp.ext.data.adg_rtd.placement', function() { const config = { name: SUBMODULE_NAME, params: { @@ -516,50 +516,50 @@ describe('Adagio Rtd Provider', function () { } }; - it('update the placement value with the adUnit.code value', function() { + it('set the adg_rtd.placement value from the adUnit[].bids adagio.params.placement value', function() { + const placement = 'placement-value'; + const configCopy = utils.deepClone(config); - configCopy.params.placementSource = PLACEMENT_SOURCES.ADUNITCODE; const bidRequest = utils.deepClone(bidReqConfig); + bidRequest.adUnits[0].bids[0].params.placement = placement; adagioRtdSubmodule.getBidRequestData(bidRequest, cb, configCopy); expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); - expect(bidRequest.adUnits[0].ortb2Imp.ext.data.placement).to.equal('div-gpt-ad-1460505748561-0'); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.placement).to.equal(placement); }); - it('update the placement value with the gpid value', function() { + it('fallback on the adUnit.code value to set the adg_rtd.placement value', function() { const configCopy = utils.deepClone(config); - configCopy.params.placementSource = PLACEMENT_SOURCES.GPID; + configCopy.params.placementSource = PLACEMENT_SOURCES.ADUNITCODE; const bidRequest = utils.deepClone(bidReqConfig); - const gpid = '/19968336/header-bid-tag-0' - utils.deepSetValue(bidRequest.adUnits[0], 'ortb2Imp.ext.gpid', gpid) adagioRtdSubmodule.getBidRequestData(bidRequest, cb, configCopy); expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); - expect(bidRequest.adUnits[0].ortb2Imp.ext.data.placement).to.equal(gpid); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.placement).to.equal('div-gpt-ad-1460505748561-0'); }); - it('update the placement value the legacy adUnit[].bids adagio.params.placement value', function() { - const placement = 'placement-value'; - + it('fallback on the the gpid value to set the adg_rtd.placement value ', function() { const configCopy = utils.deepClone(config); + configCopy.params.placementSource = PLACEMENT_SOURCES.GPID; const bidRequest = utils.deepClone(bidReqConfig); - bidRequest.adUnits[0].bids[0].params.placement = placement; + const gpid = '/19968336/header-bid-tag-0' + utils.deepSetValue(bidRequest.adUnits[0], 'ortb2Imp.ext.gpid', gpid) adagioRtdSubmodule.getBidRequestData(bidRequest, cb, configCopy); expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); - expect(bidRequest.adUnits[0].ortb2Imp.ext.data.placement).to.equal(placement); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.placement).to.equal(gpid); }); - it('it does not populate `ortb2Imp.ext.data.placement` if no fallback', function() { + it('it does not populate `ortb2Imp.ext.data.adg_rtd.placement` if no fallback', function() { const configCopy = utils.deepClone(config); const bidRequest = utils.deepClone(bidReqConfig); adagioRtdSubmodule.getBidRequestData(bidRequest, cb, configCopy); expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); - expect(bidRequest.adUnits[0].ortb2Imp.ext.data.placement).to.not.exist; + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.placement).to.not.exist; }); it('ensure we create the `ortb2Imp` object if it does not exist', function() { @@ -571,7 +571,7 @@ describe('Adagio Rtd Provider', function () { adagioRtdSubmodule.getBidRequestData(bidRequest, cb, configCopy); expect(bidRequest.adUnits[0]).to.have.property('ortb2Imp'); - expect(bidRequest.adUnits[0].ortb2Imp.ext.data.placement).to.equal('div-gpt-ad-1460505748561-0'); + expect(bidRequest.adUnits[0].ortb2Imp.ext.data.adg_rtd.placement).to.equal('div-gpt-ad-1460505748561-0'); }); }); }); diff --git a/test/spec/modules/adgridBidAdapter_spec.js b/test/spec/modules/adgridBidAdapter_spec.js index 5b6d658e1ca..3dd5d1985f5 100644 --- a/test/spec/modules/adgridBidAdapter_spec.js +++ b/test/spec/modules/adgridBidAdapter_spec.js @@ -1,9 +1,8 @@ import { expect } from 'chai'; import { - spec, STORAGE, getLocalStorage, + spec, STORAGE, getAdgridLocalStorage, } from 'modules/adgridBidAdapter.js'; import sinon from 'sinon'; -import { getAmxId } from '../../../libraries/nexx360Utils/index.js'; const sandbox = sinon.createSandbox(); describe('adgrid bid adapter tests', () => { @@ -74,8 +73,8 @@ describe('adgrid bid adapter tests', () => { sandbox.stub(STORAGE, 'localStorageIsEnabled').callsFake(() => false); }); it('We test if we get the adgridId', () => { - const output = getLocalStorage(); - expect(output).to.be.eql(false); + const output = getAdgridLocalStorage(); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -89,7 +88,7 @@ describe('adgrid bid adapter tests', () => { sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake((key) => null); }); it('We test if we get the adgridId', () => { - const output = getLocalStorage(); + const output = getAdgridLocalStorage(); expect(typeof output.adgridId).to.be.eql('string'); }); after(() => { @@ -104,8 +103,8 @@ describe('adgrid bid adapter tests', () => { sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake((key) => '{"adgridId":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4",}'); }); it('We test if we get the adgridId', () => { - const output = getLocalStorage(); - expect(output).to.be.eql(false); + const output = getAdgridLocalStorage(); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -119,7 +118,7 @@ describe('adgrid bid adapter tests', () => { sandbox.stub(STORAGE, 'getDataFromLocalStorage').callsFake((key) => '{"adgridId":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4"}'); }); it('We test if we get the adgridId', () => { - const output = getLocalStorage(); + const output = getAdgridLocalStorage(); expect(output.adgridId).to.be.eql('5ad89a6e-7801-48e7-97bb-fe6f251f6cb4'); }); after(() => { @@ -299,6 +298,8 @@ describe('adgrid bid adapter tests', () => { source: 'prebid.js', pageViewId: requestContent.ext.pageViewId, bidderVersion: '2.0', + requestCounter: 0, + sessionId: requestContent.ext.sessionId, }, cur: [ 'USD', @@ -514,6 +515,7 @@ describe('adgrid bid adapter tests', () => { mediaType: 'outstream', ssp: 'test', adUnitCode: 'div-1', + divId: 'div-1', }, }, ], @@ -536,6 +538,7 @@ describe('adgrid bid adapter tests', () => { currency: 'USD', netRevenue: true, ttl: 120, + divId: 'div-1', mediaType: 'video', meta: { advertiserDomains: ['adgrid.com'], demandSource: 'test' }, vastXml: 'vast', diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 38612a35e75..b2ca5c622fe 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -1,7 +1,7 @@ import {expect} from 'chai'; import {spec} from 'modules/adkernelBidAdapter'; import * as utils from 'src/utils'; -import * as navigatorDnt from 'libraries/navigatorData/dnt.js'; +import * as navigatorDnt from 'libraries/dnt/index.js'; import {NATIVE, BANNER, VIDEO} from 'src/mediaTypes'; import {config} from 'src/config'; import {parseDomain} from '../../../src/refererDetection.js'; diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 47434556426..6c6f08b8750 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -1,15 +1,15 @@ -import { expect } from 'chai'; -import { spec } from 'modules/adnuntiusBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; -import { config } from 'src/config.js'; +import '../../../src/prebid.js'; +import {expect} from 'chai'; +import {spec} from 'modules/adnuntiusBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import { getStorageManager } from 'src/storageManager.js'; -import { getGlobal } from '../../../src/prebidGlobal.js'; import {deepClone, getUnixTimestampFromNow} from 'src/utils.js'; -import { getWinDimensions } from '../../../src/utils.js'; +import {getStorageManager} from 'src/storageManager.js'; +import {getGlobal} from '../../../src/prebidGlobal.js'; +import {getWinDimensions} from '../../../src/utils.js'; import {getGlobalVarName} from '../../../src/buildOptions.js'; -import {getExtraWinDimensions} from '../../../libraries/extraWinDimensions/extraWinDimensions.js'; describe('adnuntiusBidAdapter', function () { const sandbox = sinon.createSandbox(); @@ -51,7 +51,6 @@ describe('adnuntiusBidAdapter', function () { const tzo = new Date().getTimezoneOffset(); const prebidVersion = getGlobal().version; - let screen; let viewport; let ENDPOINT_URL_BASE; let ENDPOINT_URL; @@ -62,15 +61,13 @@ describe('adnuntiusBidAdapter', function () { function resetExpectedUrls() { const winDimensions = getWinDimensions(); - const extraDims = getExtraWinDimensions(); - screen = extraDims.screen.availWidth + 'x' + extraDims.screen.availHeight; viewport = winDimensions.innerWidth + 'x' + winDimensions.innerHeight; - ENDPOINT_URL_BASE = `${URL}${tzo}&format=prebid&pbv=${prebidVersion}&screen=${screen}&viewport=${viewport}`; + ENDPOINT_URL_BASE = `${URL}${tzo}&format=prebid&pbv=${prebidVersion}&viewport=${viewport}`; ENDPOINT_URL = `${ENDPOINT_URL_BASE}&userId=${usi}`; - LOCALHOST_URL = `http://localhost:8078/i?tzo=${tzo}&format=prebid&pbv=${prebidVersion}&screen=${screen}&viewport=${viewport}&userId=${usi}`; + LOCALHOST_URL = `http://localhost:8078/i?tzo=${tzo}&format=prebid&pbv=${prebidVersion}&viewport=${viewport}&userId=${usi}`; ENDPOINT_URL_NOCOOKIE = `${ENDPOINT_URL_BASE}&userId=${usi}&noCookies=true`; ENDPOINT_URL_SEGMENTS = `${ENDPOINT_URL_BASE}&segments=segment1,segment2,segment3&userId=${usi}`; - ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=prebid&pbv=${prebidVersion}&consentString=consentString&gdpr=1&screen=${screen}&viewport=${viewport}&userId=${usi}`; + ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=prebid&pbv=${prebidVersion}&consentString=consentString&gdpr=1&viewport=${viewport}&userId=${usi}`; } function expectUrlsEqual(actual, expected) { @@ -888,12 +885,10 @@ describe('adnuntiusBidAdapter', function () { describe('buildRequests', function () { it('Test requests', function () { const winDimensions = getWinDimensions(); - const extraDims = getExtraWinDimensions(); - const screen = extraDims.screen.availWidth + 'x' + extraDims.screen.availHeight; const viewport = winDimensions.innerWidth + 'x' + winDimensions.innerHeight; const prebidVersion = window[getGlobalVarName()].version; const tzo = new Date().getTimezoneOffset(); - const ENDPOINT_URL = `https://ads.adnuntius.delivery/i?tzo=${tzo}&format=prebid&pbv=${prebidVersion}&screen=${screen}&viewport=${viewport}&userId=${usi}`; + const ENDPOINT_URL = `https://ads.adnuntius.delivery/i?tzo=${tzo}&format=prebid&pbv=${prebidVersion}&viewport=${viewport}&userId=${usi}`; const bidderRequests = [ { diff --git a/test/spec/modules/adoceanBidAdapter_spec.js b/test/spec/modules/adoceanBidAdapter_spec.js new file mode 100644 index 00000000000..53a82ec963d --- /dev/null +++ b/test/spec/modules/adoceanBidAdapter_spec.js @@ -0,0 +1,461 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adoceanBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { deepClone } from 'src/utils.js'; + +describe('AdoceanAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bannerBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + const invalidBid = Object.assign({}, bannerBid, {params: {masterId: 0}}); + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + const videoInscreenBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'instream', + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true for instream video', function () { + expect(spec.isBidRequestValid(videoInscreenBid)).to.equal(true); + }); + + const videoAdpodBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'adpod', + playerSize: [300, 250], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: false + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return true for adpod video without requireExactDuration', function () { + expect(spec.isBidRequestValid(videoAdpodBid)).to.equal(true); + }); + + it('should return false for adpod video with requireExactDuration', function () { + const invalidBid = Object.assign({}, videoAdpodBid); + invalidBid.mediaTypes.video.requireExactDuration = true; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + const videoOutstreamBid = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should return false for outstream video', function () { + expect(spec.isBidRequestValid(videoOutstreamBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 200], [600, 250]] + } + }, + bidId: '30b31c1838de1f', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + } + ]; + + const bidderRequest = { + gdprConsent: { + consentString: 'BOQHk-4OSlWKFBoABBPLBd-AAAAgWAHAACAAsAPQBSACmgFTAOkA', + gdprApplies: true + } + }; + + it('should send two requests if slave is duplicated', function () { + const nrOfRequests = spec.buildRequests(bidRequests, bidderRequest).length; + expect(nrOfRequests).to.equal(2); + }); + + it('should add bidIdMap with correct slaveId => bidId mapping', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (let i = 0; i < bidRequests.length; i++) { + expect(requests[i]).to.exist; + expect(requests[i].bidIdMap).to.exist; + expect(requests[i].bidIdMap[bidRequests[i].params.slaveId]).to.equal(bidRequests[i].bidId); + } + }); + + it('sends bid request to url via GET', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.match(new RegExp(`^https://${bidRequests[0].params.emitter}/_[0-9]*/ad.json`)); + }); + + it('should attach id to url', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.url).to.include('id=' + bidRequests[0].params.masterId); + }); + + it('should attach consent information to url', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.url).to.include('gdpr=1'); + expect(request.url).to.include('gdpr_consent=' + bidderRequest.gdprConsent.consentString); + }); + + it('should attach slaves information to url', function () { + let requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests[0].url).to.include('slaves=zpniqismex'); + expect(requests[1].url).to.include('slaves=zpniqismex'); + expect(requests[0].url).to.include('aosize=300x250%2C300x600'); + expect(requests[1].url).to.include('aosize=300x200%2C600x250'); + + const differentSlavesBids = deepClone(bidRequests); + differentSlavesBids[1].params.slaveId = 'adoceanmyaowafpdwlrks'; + requests = spec.buildRequests(differentSlavesBids, bidderRequest); + expect(requests.length).to.equal(2); + expect(requests[0].url).to.include('slaves=zpniqismex'); + expect(requests[1].url).to.include('slaves=wafpdwlrks'); + }); + + const videoInstreamBidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'instream', + minduration: 10, + maxduration: 60, + } + }, + bidId: '30b31c1838de1g', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a476', + } + ]; + + it('should build correct video instream request', function () { + const request = spec.buildRequests(videoInstreamBidRequests, bidderRequest)[0]; + expect(request).to.exist; + expect(request.url).to.include('id=' + videoInstreamBidRequests[0].params.masterId); + expect(request.url).to.include('slaves=zpniqismex'); + expect(request.url).to.include('spots=1'); + expect(request.url).to.include('dur=60'); + expect(request.url).to.include('maxdur=60'); + expect(request.url).to.include('mindur=10'); + }); + + const videoAdpodBidRequests = [ + { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'adpod', + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: false + } + }, + bidId: '30b31c1838de1h', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a476', + } + ]; + + it('should build correct video adpod request', function () { + const request = spec.buildRequests(videoAdpodBidRequests, bidderRequest)[0]; + expect(request).to.exist; + expect(request.url).to.include('id=' + videoAdpodBidRequests[0].params.masterId); + expect(request.url).to.include('slaves=zpniqismex'); + expect(request.url).to.include('spots=20'); // 300 / 15 = 20 + expect(request.url).to.include('dur=300'); + expect(request.url).to.include('maxdur=30'); + expect(request.url).to.not.include('mindur='); + }); + }); + + describe('interpretResponseBanner', function () { + const response = { + 'body': [ + { + 'id': 'adoceanmyaozpniqismex', + 'price': '0.019000', + 'ttl': '360', + 'crid': 'veeinoriep', + 'currency': 'EUR', + 'width': '300', + 'height': '250', + 'isVideo': false, + 'code': '%3C!--%20Creative%20--%3E', + 'adomain': ['adocean.pl'] + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaozpniqismex', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bidIdMap: { + adoceanmyaozpniqismex: '30b31c1838de1e' + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 0.019000, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'ad': '', + 'creativeId': 'veeinoriep', + 'ttl': 360, + 'netRevenue': false, + 'meta': { + 'advertiserDomains': ['adocean.pl'], + 'mediaType': 'banner' + } + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1, 'Response should contain 1 bid'); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort(), 'Response keys do not match'); + resultKeys.forEach(function(k) { + if (k === 'ad') { + expect(result[0][k]).to.match(/$/, 'ad does not match'); + } else if (k === 'meta') { + expect(result[0][k]).to.deep.equal(expectedResponse[0][k], 'meta does not match'); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k], `${k} does not match`); + } + }); + }); + + it('handles nobid responses', function () { + response.body = [ + { + 'id': 'adoceanmyaolafpjwftbz', + 'error': 'true' + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(0, 'Error response should be empty'); + }); + }); + + describe('interpretResponseVideo', function () { + const response = { + 'body': [ + { + 'id': 'adoceanmyaolifgmvmpfj', + 'price': '0.019000', + 'ttl': '360', + 'crid': 'qpqhltkgpu', + 'currency': 'EUR', + 'width': '300', + 'height': '250', + 'isVideo': true, + 'code': '%3C!--%20Video%20Creative%20--%3E', + 'adomain': ['adocean.pl'] + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + bidder: 'adocean', + params: { + masterId: 'tmYF.DMl7ZBq.Nqt2Bq4FutQTJfTpxCOmtNPZoQUDcL.G7', + slaveId: 'adoceanmyaolifgmvmpfj', + emitter: 'myao.adocean.pl' + }, + adUnitCode: 'adunit-code', + bidIdMap: { + adoceanmyaolifgmvmpfj: '30b31c1838de1e' + }, + mediaTypes: { + video: { + playerSize: [200, 200], + context: 'instream' + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '30b31c1838de1e', + 'cpm': 0.019000, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'vastXml': '', + 'creativeId': 'qpqhltkgpu', + 'ttl': 360, + 'netRevenue': false, + 'meta': { + 'advertiserDomains': ['adocean.pl'], + 'mediaType': 'video' + } + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1, 'Response should contain 1 bid'); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort(), 'Response keys do not match'); + resultKeys.forEach(function(k) { + if (k === 'vastXml') { + expect(result[0][k]).to.match(/$/, 'vastXml does not match'); + } else if (k === 'meta') { + expect(result[0][k]).to.deep.equal(expectedResponse[0][k], 'meta does not match'); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k], `${k} does not match`); + } + }); + }); + + it('handles nobid responses', function () { + response.body = [ + { + 'id': 'adoceanmyaolafpjwftbz', + 'error': 'true' + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(0, 'Error response should be empty'); + }); + }); +}); diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index a6949d5c5a3..310969c370a 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -1,5 +1,7 @@ import { expect } from 'chai' -import { ENDPOINT, spec } from 'modules/alkimiBidAdapter.js' +import sinon from 'sinon' +import { config } from 'src/config.js' +import { ENDPOINT, spec, storage } from 'modules/alkimiBidAdapter.js' import { newBidder } from 'src/adapters/bidderFactory.js' const REQUEST = { @@ -114,6 +116,16 @@ describe('alkimiBidAdapter', function () { }) describe('buildRequests', function () { + let sandbox; + + beforeEach(function() { + sandbox = sinon.createSandbox(); + }); + + afterEach(function() { + sandbox.restore(); + }); + const bidRequests = [REQUEST] const requestData = { refererInfo: { @@ -160,7 +172,11 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.data.requestId).to.not.equal(undefined) expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.equal({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', bidFloor: 0.1, sizes: [{ width: 300, height: 250 }], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode', instl: undefined, exp: undefined, banner: { sizes: [[300, 250]] }, video: undefined, ext: { gpid: '/111/banner#300x250', tid: 'e64782a4-8e68-4c38-965b-80ccf115d46a' } }) + expect(bidderRequest.data.signRequest.bids[0]).to.include({ + token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', + bidFloor: 0.1, + currency: 'USD' + }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) @@ -170,17 +186,209 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.url).to.equal(ENDPOINT) }) - it('sends bidFloor when configured', () => { - const requestWithFloor = Object.assign({}, REQUEST); - requestWithFloor.getFloor = function (arg) { - if (arg.currency === 'USD' && arg.mediaType === 'banner' && JSON.stringify(arg.size) === JSON.stringify([300, 250])) { - return { currency: 'USD', floor: 0.3 } - } - } - const bidderRequestFloor = spec.buildRequests([requestWithFloor], requestData); - expect(bidderRequestFloor.data.signRequest.bids[0].bidFloor).to.be.equal(0.3); + // Wallet Profiling Test Cases + describe('Wallet Profiling', function () { + it('should include all wallet parameters when alkimi config is complete', function () { + const alkimiConfigStub = { + userWalletAddress: '0x1234567890abcdef', + userParams: { segment: 'premium', interests: ['crypto', 'defi'] }, + userWalletConnected: 'true', + userWalletProtocol: 'ERC-20', + userTokenType: 'Alkimi', + signature: 'test-signature', + randomUUID: 'test-uuid-123' + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user).to.exist; + expect(bidderRequest.data.ortb2.user.ext.userWalletAddress).to.equal('0x1234567890abcdef'); + expect(bidderRequest.data.ortb2.user.ext.userParams).to.deep.equal({ segment: 'premium', interests: ['crypto', 'defi'] }); + expect(bidderRequest.data.ortb2.user.ext.userWalletConnected).to.equal('true'); + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.deep.equal(['ERC-20']); + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.deep.equal(['Alkimi']); + expect(bidderRequest.data.signature).to.equal('test-signature'); + expect(bidderRequest.data.signRequest.randomUUID).to.equal('test-uuid-123'); + }); + + it('should handle comma-separated string values for wallet protocol and token type', function () { + const alkimiConfigStub = { + userWalletAddress: '0xtest', + userWalletProtocol: 'ERC-20, TRC-20, BSC', + userTokenType: 'Alkimi, USDT, ETH' + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.deep.equal(['ERC-20', 'TRC-20', 'BSC']); + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.deep.equal(['Alkimi', 'USDT', 'ETH']); + }); + + it('should handle array values for wallet protocol and token type', function () { + const alkimiConfigStub = { + userWalletAddress: '0xtest', + userWalletProtocol: ['ERC-20', 'TRC-20'], + userTokenType: ['Alkimi', 'USDT'] + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.deep.equal(['ERC-20', 'TRC-20']); + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.deep.equal(['Alkimi', 'USDT']); + }); + + it('should handle single string value (non-comma) for wallet protocol and token type', function () { + const alkimiConfigStub = { + userWalletAddress: '0xtest', + userWalletProtocol: 'ERC20', + userTokenType: 'Alkimi' + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.deep.equal(['ERC20']); + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.deep.equal(['Alkimi']); + }); + + it('should handle partial wallet config with only some parameters', function () { + const alkimiConfigStub = { + userWalletAddress: '0xpartial', + userWalletConnected: 'false' + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user).to.exist; + expect(bidderRequest.data.ortb2.user.ext.userWalletAddress).to.equal('0xpartial'); + expect(bidderRequest.data.ortb2.user.ext.userWalletConnected).to.equal('false'); + expect(bidderRequest.data.ortb2.user.ext.userParams).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.be.undefined; + }); + + it('should include user object with only userId when no wallet config exists', function () { + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(undefined); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage').withArgs('alkimiUserID').returns('stored-user-id'); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user).to.exist; + expect(bidderRequest.data.ortb2.user.id).to.equal('stored-user-id'); + expect(bidderRequest.data.ortb2.user.ext.userWalletAddress).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userParams).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userWalletConnected).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.be.undefined; + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.be.undefined; + }); + + it('should not include user object when no wallet config and no userId exists', function () { + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(undefined); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user).to.be.undefined; + }); + + it('should filter out empty strings from comma-separated values', function () { + const alkimiConfigStub = { + userWalletAddress: '0xtest', + userWalletProtocol: 'ERC-20, , TRC-20, ', + userTokenType: 'Alkimi, , , ETH' + }; + + sandbox.stub(config, 'getConfig').withArgs('alkimi').returns(alkimiConfigStub); + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + + const bidderRequest = spec.buildRequests(bidRequests, requestData); + + expect(bidderRequest.data.ortb2.user.ext.userWalletProtocol).to.deep.equal(['ERC-20', 'TRC-20']); + expect(bidderRequest.data.ortb2.user.ext.userTokenType).to.deep.equal(['Alkimi', 'ETH']); + }); }); - }) + + // Currency Test Cases + describe('Multi-Currency Support', function () { + it('should handle floor with default USD currency', function () { + const requestWithFloor = Object.assign({}, REQUEST); + requestWithFloor.getFloor = function (arg) { + if (arg.currency === 'USD' && arg.mediaType === 'banner' && JSON.stringify(arg.size) === JSON.stringify([300, 250])) { + return { currency: 'USD', floor: 0.3 }; + } + }; + + const bidderRequestFloor = spec.buildRequests([requestWithFloor], requestData); + expect(bidderRequestFloor.data.signRequest.bids[0].bidFloor).to.equal(0.3); + expect(bidderRequestFloor.data.signRequest.bids[0].currency).to.equal('USD'); + }); + + it('should handle floor with EUR currency config', function () { + const requestWithFloor = Object.assign({}, REQUEST); + requestWithFloor.getFloor = function (arg) { + if (arg.currency === 'EUR' && arg.mediaType === 'banner') { + return { currency: 'EUR', floor: 2.0 }; + } + }; + + sandbox.stub(config, 'getConfig').withArgs('currency').returns({ adServerCurrency: 'EUR' }); + + const bidderRequest = spec.buildRequests([requestWithFloor], requestData); + + expect(bidderRequest.data.signRequest.bids[0].bidFloor).to.equal(2.0); + expect(bidderRequest.data.signRequest.bids[0].currency).to.equal('EUR'); + }); + + it('should use params.bidFloor with default currency when getFloor is not available', function () { + const requestWithoutGetFloor = Object.assign({}, REQUEST); + delete requestWithoutGetFloor.getFloor; + requestWithoutGetFloor.params.bidFloor = 0.5; + + const bidderRequest = spec.buildRequests([requestWithoutGetFloor], requestData); + + expect(bidderRequest.data.signRequest.bids[0].bidFloor).to.equal(0.5); + expect(bidderRequest.data.signRequest.bids[0].currency).to.equal('USD'); + }); + + it('should select minimum floor when multiple media types are present', function () { + const multiFormatRequest = Object.assign({}, REQUEST, { + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { playerSize: [[640, 480]] } + } + }); + + multiFormatRequest.getFloor = function (arg) { + if (arg.mediaType === 'banner') { + return { currency: 'USD', floor: 2.5 }; + } + if (arg.mediaType === 'video') { + return { currency: 'USD', floor: 1.8 }; + } + }; + + const bidderRequest = spec.buildRequests([multiFormatRequest], requestData); + + expect(bidderRequest.data.signRequest.bids[0].bidFloor).to.equal(1.8); + expect(bidderRequest.data.signRequest.bids[0].currency).to.equal('USD'); + }); + }); + }); describe('interpretResponse', function () { it('handles banner request : should get correct bid response', function () { @@ -227,6 +435,49 @@ describe('alkimiBidAdapter', function () { result = spec.interpretResponse({ body: BIDDER_NO_BID_RESPONSE }, {}) expect(result).to.deep.equal([]) }) + + it('should handle response with explicit currency', function () { + const responseWithCurrency = { + prebidResponse: [{ + ad: '

test
', + requestId: 'test-req-1', + cpm: 1.5, + currency: 'EUR', + width: 300, + height: 250, + ttl: 300, + creativeId: 1, + netRevenue: true, + mediaType: 'banner' + }] + }; + + const result = spec.interpretResponse({ body: responseWithCurrency }, {}); + + expect(result[0].currency).to.equal('EUR'); + expect(result[0].cpm).to.equal(1.5); + }); + + it('should default to USD when currency is not in response', function () { + const responseWithoutCurrency = { + prebidResponse: [{ + ad: '
test
', + requestId: 'test-req-2', + cpm: 2.0, + width: 300, + height: 250, + ttl: 300, + creativeId: 2, + netRevenue: true, + mediaType: 'banner' + }] + }; + + const result = spec.interpretResponse({ body: responseWithoutCurrency }, {}); + + expect(result[0].currency).to.equal('USD'); + expect(result[0].cpm).to.equal(2.0); + }); }) describe('onBidWon', function () { diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index d1e88b35a18..1328a28e438 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -401,6 +401,23 @@ describe('AmxBidAdapter', () => { }, { ...base, t: 3 }, ], + [ + { + all: { + bidders: ['amx'], + }, + }, + { ...base, t: 3 }, + ], + [ + { + all: { + bidders: '*', + filter: 'include', + }, + }, + { ...base, t: 3 }, + ], [ { image: { diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index 4b8b908a792..c294a5e08d0 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import atsAnalyticsAdapter, {parseBrowser, analyticsUrl} from '../../../modules/atsAnalyticsAdapter.js'; +import atsAnalyticsAdapter, {parseBrowser, analyticsUrl, bidRequestedHandler} from '../../../modules/atsAnalyticsAdapter.js'; import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import {server} from '../../mocks/xhr.js'; @@ -178,9 +178,12 @@ describe('ats analytics adapter', function () { // Step 6: Send bid won event events.emit(EVENTS.BID_WON, wonRequest); - sandbox.stub(getGlobal(), 'getAllWinningBids').callsFake((key) => { - return [wonRequest] - }); + // Stub getAllWinningBids before auction end processing + const globalObj = getGlobal(); + if (typeof globalObj.getAllWinningBids !== 'function') { + globalObj.getAllWinningBids = function() { return []; }; + } + sandbox.stub(globalObj, 'getAllWinningBids').returns([wonRequest]); clock.tick(2000); @@ -274,5 +277,208 @@ describe('ats analytics adapter', function () { }); expect(utils.logError.called).to.equal(true); }) + + describe('has_envelope logic for Prebid v10.0+ compatibility', function () { + beforeEach(function () { + sinon.stub(atsAnalyticsAdapter, 'getUserAgent').returns('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/536.25 (KHTML, like Gecko) Version/6.0 Safari/536.25'); + sinon.stub(Math, 'random').returns(0.99); + storage.setCookie('_lr_env_src_ats', 'true', 'Thu, 01 Jan 1970 00:00:01 GMT'); + + // Enable analytics for testing + atsAnalyticsAdapter.enableAnalytics({ + options: { pid: '10433394' } + }); + }); + + it('should return true when userIdAsEids contains liveramp.com source with valid uids', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [{ + 'source': 'liveramp.com', + 'uids': [{'id': 'AmThEbO1ssIWjrNdU4noT4ZFBILSVBBYHbipOYt_JP40e5nZdXns2g'}] + }] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(true); + }); + + it('should return false when userIdAsEids contains liveramp source but no uids', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [{ + 'source': 'liveramp.com', + 'uids': [] + }] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return false when userIdAsEids contains non-liveramp sources', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [{ + 'source': 'id5-sync.com', + 'uids': [{'id': 'some-other-id'}] + }] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return false when userIdAsEids is empty array', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return false when userIdAsEids is not present', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17' + // No userIdAsEids property + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return false when userIdAsEids is not an array', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': 'not-an-array' + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return true for legacy userId.idl_env (backward compatibility)', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userId': { + 'idl_env': 'AmThEbO1ssIWjrNdU4noT4ZFBILSVBBYHbipOYt_JP40e5nZdXns2g' + } + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(true); + }); + + it('should return false when userIdAsEids has liveramp source but uids is null', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [{ + 'source': 'liveramp.com', + 'uids': null + }] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should return false when userIdAsEids has liveramp source but no uids property', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [{ + 'source': 'liveramp.com' + // No uids property + }] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(false); + }); + + it('should handle multiple userIdAsEids entries and find liveramp source', function () { + const bidRequestArgs = { + 'bidderCode': 'appnexus', + 'auctionStart': 1580739265161, + 'bids': [{ + 'bidder': 'appnexus', + 'bidId': '30c77d079cdf17', + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [{'id': 'id5-value'}] + }, + { + 'source': 'liveramp.com', + 'uids': [{'id': 'liveramp-value'}] + }, + { + 'source': 'criteo.com', + 'uids': [{'id': 'criteo-value'}] + } + ] + }], + 'auctionId': 'a5b849e5-87d7-4205-8300-d063084fcfb7' + }; + + const result = bidRequestedHandler(bidRequestArgs); + expect(result[0].has_envelope).to.equal(true); + }); + }) }) }) diff --git a/test/spec/modules/bidResponseFilter_spec.js b/test/spec/modules/bidResponseFilter_spec.js index 6251fe35250..c37003bde50 100644 --- a/test/spec/modules/bidResponseFilter_spec.js +++ b/test/spec/modules/bidResponseFilter_spec.js @@ -3,11 +3,12 @@ import { BID_ADV_DOMAINS_REJECTION_REASON, BID_ATTR_REJECTION_REASON, BID_CATEGORY_REJECTION_REASON, - init, - MODULE_NAME - , reset} from '../../../modules/bidResponseFilter/index.js'; -import {config} from '../../../src/config.js'; -import {addBidResponse} from '../../../src/auction.js'; + BID_MEDIA_TYPE_REJECTION_REASON, + MODULE_NAME, + reset +} from '../../../modules/bidResponseFilter/index.js'; +import { addBidResponse } from '../../../src/auction.js'; +import { config } from '../../../src/config.js'; describe('bidResponseFilter', () => { let mockAuctionIndex @@ -56,11 +57,19 @@ describe('bidResponseFilter', () => { badv: [], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] }); + mockAuctionIndex.getBidRequest = () => ({ + mediaTypes: { + banner: {} + }, + ortb2Imp: {} + }) + const bid = { meta: { advertiserDomains: ['domain1.com', 'domain2.com'], primaryCatId: 'EXAMPLE-CAT-ID', - attr: 'attr' + attr: 'attr', + mediaType: 'banner' } }; @@ -138,10 +147,18 @@ describe('bidResponseFilter', () => { meta: { advertiserDomains: ['validdomain1.com', 'validdomain2.com'], primaryCatId: 'BANNED_CAT1', - attr: 'valid_attr' + attr: 'valid_attr', + mediaType: 'banner', } }; + mockAuctionIndex.getBidRequest = () => ({ + mediaTypes: { + banner: {} + }, + ortb2Imp: {} + }) + mockAuctionIndex.getOrtb2 = () => ({ badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] }); @@ -159,7 +176,8 @@ describe('bidResponseFilter', () => { meta: { advertiserDomains: ['validdomain1.com', 'validdomain2.com'], primaryCatId: undefined, - attr: 'valid_attr' + attr: 'valid_attr', + mediaType: 'banner' } }; @@ -167,10 +185,53 @@ describe('bidResponseFilter', () => { badv: ['domain2.com'], bcat: ['BANNED_CAT1', 'BANNED_CAT2'] }); + mockAuctionIndex.getBidRequest = () => ({ + mediaTypes: { + banner: {} + }, + ortb2Imp: {} + }) + config.setConfig({[MODULE_NAME]: {cat: {blockUnknown: false}}}); addBidResponseHook(call, 'adcode', bid, () => { - }); + }, mockAuctionIndex); sinon.assert.calledOnce(call); }); + + it('should reject bid for meta.mediaType not present on adunit', () => { + const reject = sinon.stub(); + const call = sinon.stub(); + const bid = { + meta: { + advertiserDomains: ['validdomain1.com', 'validdomain2.com'], + primaryCatId: 'VALID_CAT', + attr: 6, + mediaType: 'audio' + }, + }; + + mockAuctionIndex.getOrtb2 = () => ({ + badv: [], bcat: [] + }); + + mockAuctionIndex.getBidRequest = () => ({ + ortb2Imp: { + banner: { + }, + video: { + }, + } + }) + + mockAuctionIndex.getBidRequest = () => ({ + mediaTypes: { + banner: {}, + }, + ortb2Imp: {} + }) + + addBidResponseHook(call, 'adcode', bid, reject, mockAuctionIndex); + sinon.assert.calledWith(reject, BID_MEDIA_TYPE_REJECTION_REASON); + }); }) diff --git a/test/spec/modules/clickioBidAdapter_spec.js b/test/spec/modules/clickioBidAdapter_spec.js new file mode 100644 index 00000000000..d35ded253e1 --- /dev/null +++ b/test/spec/modules/clickioBidAdapter_spec.js @@ -0,0 +1,177 @@ +import { expect } from 'chai'; +import { spec } from 'modules/clickioBidAdapter.js'; + +describe('clickioBidAdapter', function () { + const DEFAULT_BANNER_BID_REQUESTS = [ + { + adUnitCode: 'div-banner-id', + bidId: 'bid-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + ], + }, + }, + bidder: 'clickio', + params: {}, + requestId: 'request-123', + } + ]; + + const DEFAULT_BANNER_BIDDER_REQUEST = { + bidderCode: 'clickio', + bids: DEFAULT_BANNER_BID_REQUESTS, + }; + + const SAMPLE_RESPONSE = { + body: { + id: '12h712u7-k22g-8124-ab7a-h268s22dy271', + seatbid: [ + { + bid: [ + { + id: '1bh7jku7-ko2g-8654-ab72-h268abcde271', + impid: 'bid-123', + price: 1.5, + adm: '
Test Ad
', + adomain: ['example.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 250, + mtype: 1, + }, + ], + seat: '4212', + }, + ], + cur: 'USD', + }, + }; + + describe('isBidRequestValid', () => { + it('should return true for any valid bid request', () => { + const bidRequest = { + params: {}, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + }); + + describe('buildRequests', () => { + it('should build a valid request object', () => { + const request = spec.buildRequests( + DEFAULT_BANNER_BID_REQUESTS, + DEFAULT_BANNER_BIDDER_REQUEST + ); + + expect(request).to.be.an('array'); + expect(request).to.have.lengthOf(1); + expect(request[0].method).to.equal('POST'); + expect(request[0].url).to.equal('https://o.clickiocdn.com/bids'); + expect(request[0].data).to.be.an('object'); + }); + + it('should include imp with ext.params from bidRequest', () => { + const bidRequestsWithParams = [ + { + ...DEFAULT_BANNER_BID_REQUESTS[0], + params: { + said: '123', + someParam: 'value' + } + } + ]; + const bidderRequest = { ...DEFAULT_BANNER_BIDDER_REQUEST, bids: bidRequestsWithParams }; + const request = spec.buildRequests(bidRequestsWithParams, bidderRequest)[0]; + + expect(request.data.imp).to.be.an('array'); + expect(request.data.imp[0].ext.params).to.deep.equal({ + said: '123', + someParam: 'value' + }); + }); + }); + + describe('interpretResponse', () => { + it('should return valid bids from ORTB response', () => { + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(SAMPLE_RESPONSE, request); + + expect(bids).to.be.an('array'); + expect(bids).to.have.lengthOf(1); + + const bid = bids[0]; + expect(bid.requestId).to.exist; + expect(bid.cpm).to.equal(1.5); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ad).to.contain('
Test Ad
'); + expect(bid.creativeId).to.equal('535231'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(30); + }); + + it('should return empty array if no bids in response', () => { + const emptyResponse = { + body: { + id: '12h712u7-k22g-8124-ab7a-h268s22dy271', + seatbid: [], + cur: 'USD', + }, + }; + const request = spec.buildRequests(DEFAULT_BANNER_BID_REQUESTS, DEFAULT_BANNER_BIDDER_REQUEST)[0]; + const bids = spec.interpretResponse(emptyResponse, request); + + expect(bids).to.be.an('array'); + expect(bids).to.be.empty; + }); + }); + + describe('getUserSyncs', () => { + it('should return iframe user sync', () => { + const syncOptions = { iframeEnabled: true }; + const syncs = spec.getUserSyncs(syncOptions); + + expect(syncs).to.be.an('array'); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include('https://o.clickiocdn.com/cookie_sync_html'); + }); + + it('should include GDPR parameters when gdprConsent is provided', () => { + const syncOptions = { iframeEnabled: true }; + const gdprConsent = { + gdprApplies: true, + consentString: 'test-consent-string' + }; + const syncs = spec.getUserSyncs(syncOptions, null, gdprConsent); + + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=test-consent-string'); + }); + + it('should include USP consent when uspConsent is provided', () => { + const syncOptions = { iframeEnabled: true }; + const uspConsent = '1YNN'; + const syncs = spec.getUserSyncs(syncOptions, null, null, uspConsent); + + expect(syncs[0].url).to.include('us_privacy=1YNN'); + }); + + it('should include GPP parameters when gppConsent is provided', () => { + const syncOptions = { iframeEnabled: true }; + const gppConsent = { + gppString: 'DBACNYA~CPXxRfAPXxR', + applicableSections: [7, 8] + }; + const syncs = spec.getUserSyncs(syncOptions, null, null, null, gppConsent); + + expect(syncs[0].url).to.include('gpp=DBACNYA~CPXxRfAPXxR'); + expect(syncs[0].url).to.include('gpp_sid=7'); + expect(syncs[0].url).to.include('gpp_sid=8'); + }); + }); +}); diff --git a/test/spec/modules/cointrafficBidAdapter_spec.js b/test/spec/modules/cointrafficBidAdapter_spec.js index e90216d9612..78fbfadddc4 100644 --- a/test/spec/modules/cointrafficBidAdapter_spec.js +++ b/test/spec/modules/cointrafficBidAdapter_spec.js @@ -9,7 +9,7 @@ import * as utils from 'src/utils.js' * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest */ -const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps.adsgravity.io/v1/request/prebid'; describe('cointrafficBidAdapter', function () { describe('isBidRequestValid', function () { @@ -20,9 +20,13 @@ describe('cointrafficBidAdapter', function () { placementId: 'testPlacementId' }, adUnitCode: 'adunit-code', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, bidId: 'bidId12345', bidderRequestId: 'bidderRequestId12345', auctionId: 'auctionId12345' @@ -42,9 +46,13 @@ describe('cointrafficBidAdapter', function () { placementId: 'testPlacementId' }, adUnitCode: 'adunit-code', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, bidId: 'bidId12345', bidderRequestId: 'bidderRequestId12345', auctionId: 'auctionId12345' @@ -55,9 +63,13 @@ describe('cointrafficBidAdapter', function () { placementId: 'testPlacementId' }, adUnitCode: 'adunit-code2', - sizes: [ - [300, 250] - ], + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, bidId: 'bidId67890"', bidderRequestId: 'bidderRequestId67890', auctionId: 'auctionId12345' diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index b65096068fc..48ef3a30fe3 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -77,10 +77,14 @@ describe('Yahoo ConnectID Submodule', () => { removeLocalStorageDataStub.restore(); }); - function invokeGetIdAPI(configParams, consentData) { - const result = connectIdSubmodule.getId({ + function invokeGetIdAPI(configParams, consentData, storageConfig) { + const config = { params: configParams - }, consentData); + }; + if (storageConfig) { + config.storage = storageConfig; + } + const result = connectIdSubmodule.getId(config, consentData); if (typeof result === 'object' && result.callback) { result.callback(sinon.stub()); } @@ -803,6 +807,130 @@ describe('Yahoo ConnectID Submodule', () => { expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); }); + + it('stores the result in localStorage only when storage type is html5', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'html5only'}; + const expectedStoredData = { + connectid: 'html5only', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.called).to.be.false; + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); + }); + + it('stores the result in cookie only when storage type is cookie', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'cookieonly'}; + const expectedStoredData = { + connectid: 'cookieonly', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + const expiryDelta = new Date(60 * 60 * 24 * 365 * 1000); + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'cookie'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.calledOnce).to.be.true; + expect(setCookieStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setCookieStub.firstCall.args[1]).to.equal(JSON.stringify(expectedStoredData)); + expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); + expect(setLocalStorageStub.called).to.be.false; + }); + + it('does not sync localStorage to cookie when storage type is html5', () => { + const localStorageData = {connectId: 'foobarbaz'}; + getLocalStorageStub.withArgs(STORAGE_KEY).returns(localStorageData); + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + + expect(setCookieStub.called).to.be.false; + }); + + it('updates existing ID with html5 storage type without writing cookie', () => { + const last13Days = Date.now() - (60 * 60 * 24 * 1000 * 13); + const cookieData = {connectId: 'foobar', he: HASHED_EMAIL, lastSynced: last13Days}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(20); + const newCookieData = Object.assign({}, cookieData, {lastUsed: 20}) + const result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + dateNowStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id'); + expect(setCookieStub.called).to.be.false; + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.equal(JSON.stringify(newCookieData)); + }); + + it('stores the result in both storages when storage type is cookie&html5', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'both'}; + const expectedStoredData = { + connectid: 'both', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + const expiryDelta = new Date(60 * 60 * 24 * 365 * 1000); + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'cookie&html5'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.calledOnce).to.be.true; + expect(setCookieStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setCookieStub.firstCall.args[1]).to.equal(JSON.stringify(expectedStoredData)); + expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); + }); }); }); describe('userHasOptedOut()', () => { diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 6adf159ff55..e143311a880 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -211,6 +211,7 @@ describe('consentManagementGpp', function () { } }; gppClient = mockClient(); + gppDataHandler.enable(); }); describe('updateConsent', () => { diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index a033c56ddcd..dfe5f3d6a0e 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,4 @@ -import {consentConfig, gdprScope, resetConsentData, setConsentConfig, } from 'modules/consentManagementTcf.js'; +import {consentConfig, gdprScope, resetConsentData, setConsentConfig, tcfCmpEventManager} from 'modules/consentManagementTcf.js'; import {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {config} from 'src/config.js'; @@ -736,6 +736,125 @@ describe('consentManagement', function () { expect(consent.gdprApplies).to.be.true; expect(consent.apiVersion).to.equal(2); }); + + it('should set CMP listener ID when listenerId is provided in tcfData', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: 123 + }; + + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpListenerIdSpy); + sinon.assert.calledWith(setCmpListenerIdSpy, 123); + + setCmpListenerIdSpy.restore(); + }); + + it('should not set CMP listener ID when listenerId is null', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: null + }; + + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.notCalled(setCmpListenerIdSpy); + + setCmpListenerIdSpy.restore(); + }); + + it('should not set CMP listener ID when listenerId is undefined', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: undefined + }; + + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.notCalled(setCmpListenerIdSpy); + setCmpListenerIdSpy.restore(); + }); + + it('should set CMP listener ID when listenerId is 0 (valid listener ID)', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + listenerId: 0 + }; + + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpListenerIdSpy = sinon.spy(tcfCmpEventManager, 'setCmpListenerId'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpListenerIdSpy); + sinon.assert.calledWith(setCmpListenerIdSpy, 0); + setCmpListenerIdSpy.restore(); + }); + + it('should set CMP API reference when CMP is found', async function () { + const testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded' + }; + + // Create a spy that will be applied when tcfCmpEventManager is created + let setCmpApiSpy = sinon.spy(tcfCmpEventManager, 'setCmpApi'); + + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + await setConsentConfig(goodConfig); + expect(await runHook()).to.be.true; + + sinon.assert.calledOnce(setCmpApiSpy); + expect(setCmpApiSpy.getCall(0).args[0]).to.be.a('function'); + setCmpApiSpy.restore(); + }); }); }); }); diff --git a/test/spec/modules/craftBidAdapter_spec.js b/test/spec/modules/craftBidAdapter_spec.js index 92aa103093a..45922e1cc25 100644 --- a/test/spec/modules/craftBidAdapter_spec.js +++ b/test/spec/modules/craftBidAdapter_spec.js @@ -89,12 +89,28 @@ describe('craftAdapter', function () { bidderRequestId: '4a859978b5d4bd', auctionId: '8720f980-4639-4150-923a-e96da2f1de36', transactionId: 'e0c52da2-c008-491c-a910-c6765d948700', + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [], + }, + }, + }, + }, + userIdAsEids: [ + {source: 'foobar1.com', uids: [{id: 'xxxxxxx', atype: 1}]}, + {source: 'foobar2.com', uids: [{id: 'yyyyyyy', atype: 1}]}, + ], }]; const bidderRequest = { refererInfo: { topmostLocation: 'https://www.gacraft.jp/publish/craft-prebid-example.html' } }; + it('sends bid request to ENDPOINT via POST', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.method).to.equal('POST'); @@ -111,6 +127,17 @@ describe('craftAdapter', function () { expect(data.referrer_detection).to.deep.equals({ rd_ref: 'https://www.gacraft.jp/publish/craft-prebid-example.html' }); + expect(data.schain).to.deep.equals({ + complete: 1, + nodes: [], + ver: '1.0', + }); + expect(data.user).to.deep.equals({ + eids: [ + {source: 'foobar1.com', uids: [{id: 'xxxxxxx', atype: 1}]}, + {source: 'foobar2.com', uids: [{id: 'yyyyyyy', atype: 1}]}, + ] + }); }); }); diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 3cbf8a63125..1a514d33155 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -29,6 +29,9 @@ describe("C-WIRE bid adapter", () => { transactionId: "04f2659e-c005-4eb1-a57c-fa93145e3843", }, ]; + const bidderRequest = { + pageViewId: "326dca71-9ca0-4e8f-9e4d-6106161ac1ad" + } const response = { body: { cwid: "2ef90743-7936-4a82-8acf-e73382a64e94", @@ -67,7 +70,7 @@ describe("C-WIRE bid adapter", () => { }); describe("buildRequests", function () { it("sends bid request to ENDPOINT via POST", function () { - const request = spec.buildRequests(bidRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.url).to.equal(BID_ENDPOINT); expect(request.method).to.equal("POST"); }); @@ -89,7 +92,7 @@ describe("C-WIRE bid adapter", () => { // set from bid.params const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); expect(payload.cwcreative).to.exist; expect(payload.cwcreative).to.deep.equal("str-str"); @@ -110,7 +113,7 @@ describe("C-WIRE bid adapter", () => { it("width and height should be set", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); const el = document.getElementById(`${bidRequest.adUnitCode}`); @@ -142,7 +145,7 @@ describe("C-WIRE bid adapter", () => { it("css maxWidth should be set", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); const el = document.getElementById(`${bidRequest.adUnitCode}`); @@ -167,7 +170,7 @@ describe("C-WIRE bid adapter", () => { it("read from url parameter", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); logInfo(JSON.stringify(payload)); @@ -190,7 +193,7 @@ describe("C-WIRE bid adapter", () => { it("read from url parameter", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); logInfo(JSON.stringify(payload)); @@ -213,7 +216,7 @@ describe("C-WIRE bid adapter", () => { it("read from url parameter", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); logInfo(JSON.stringify(payload)); @@ -238,7 +241,7 @@ describe("C-WIRE bid adapter", () => { it("cw_id is set", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); logInfo(JSON.stringify(payload)); @@ -263,7 +266,7 @@ describe("C-WIRE bid adapter", () => { it("pageId flattened", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); logInfo(JSON.stringify(payload)); @@ -305,7 +308,7 @@ describe("C-WIRE bid adapter", () => { it("build request adds pageId", function () { const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); expect(payload.slots[0].pageId).to.exist; @@ -392,7 +395,7 @@ describe("C-WIRE bid adapter", () => { sandbox.stub(autoplayLib, "isAutoplayEnabled").returns(true); const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); expect(payload.slots[0].params.autoplay).to.equal(true); @@ -402,7 +405,7 @@ describe("C-WIRE bid adapter", () => { sandbox.stub(autoplayLib, "isAutoplayEnabled").returns(false); const bidRequest = deepClone(bidRequests[0]); - const request = spec.buildRequests([bidRequest]); + const request = spec.buildRequests([bidRequest], bidderRequest); const payload = JSON.parse(request.data); expect(payload.slots[0].params.autoplay).to.equal(false); diff --git a/test/spec/modules/datawrkzAnalyticsAdapter_spec.js b/test/spec/modules/datawrkzAnalyticsAdapter_spec.js index 630c189a18a..1eebbfc9efb 100644 --- a/test/spec/modules/datawrkzAnalyticsAdapter_spec.js +++ b/test/spec/modules/datawrkzAnalyticsAdapter_spec.js @@ -25,7 +25,13 @@ describe("DatawrkzAnalyticsAdapter", function () { sandbox = sinon.createSandbox(); fetchStub = sandbox.stub(window, "fetch"); - adapterManager.enableAnalytics({ provider: "datawrkzanalytics" }); + adapterManager.enableAnalytics({ + provider: "datawrkzanalytics", + options: { + publisherId: "testPublisher", + apiKey: "testApiKey" + } + }); }); afterEach(function () { @@ -126,6 +132,8 @@ describe("DatawrkzAnalyticsAdapter", function () { expect(options.headers["Content-Type"]).to.equal("application/json"); const body = JSON.parse(options.body); + expect(body.publisherId).to.equal("testPublisher"); + expect(body.apiKey).to.equal("testApiKey"); expect(body.auctionId).to.equal(auctionId); expect(body.adunits[0].code).to.equal(adUnitCode); expect(body.adunits[0].bids[0].bidder).to.equal(bidder); @@ -148,6 +156,8 @@ describe("DatawrkzAnalyticsAdapter", function () { const payload = JSON.parse(options.body); expect(payload.eventType).to.equal(AD_RENDER_SUCCEEDED); + expect(payload.publisherId).to.equal("testPublisher"); + expect(payload.apiKey).to.equal("testApiKey"); expect(payload.bidderCode).to.equal("appnexus"); expect(payload.successDoc).to.be.a("string"); expect(payload.failureReason).to.be.null; @@ -170,6 +180,8 @@ describe("DatawrkzAnalyticsAdapter", function () { const payload = JSON.parse(options.body); expect(payload.eventType).to.equal(AD_RENDER_FAILED); + expect(payload.publisherId).to.equal("testPublisher"); + expect(payload.apiKey).to.equal("testApiKey"); expect(payload.bidderCode).to.equal("appnexus"); expect(payload.successDoc).to.be.null; expect(payload.failureReason).to.equal("network"); diff --git a/test/spec/modules/defineMediaBidAdapter_spec.js b/test/spec/modules/defineMediaBidAdapter_spec.js new file mode 100755 index 00000000000..a2adc024526 --- /dev/null +++ b/test/spec/modules/defineMediaBidAdapter_spec.js @@ -0,0 +1,813 @@ +// jshint esversion: 6, es3: false, node: true +import { assert } from 'chai'; +import { spec } from 'modules/defineMediaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('Define Media Bid Adapter', function () { + const mockValidBids = [ + { + "bidder": "defineMedia", + "params": { + "supplierDomainName": "traffective.com", + "devMode": false + }, + "mediaTypes": { + "banner": { + "sizes": [ + [ + 1, + 1 + ], + [ + 300, + 250 + ] + ] + } + }, + "adUnitCode": "custom-adunit-code", + "transactionId": "9af02bbf-558f-4328-a7b3-0b67bac44dbc", + "adUnitId": "e9a971c1-7ce9-4bcf-8b64-611e79f6e35c", + "sizes": [ + [ + 1, + 1 + ], + [ + 300, + 250 + ] + ], + "bidId": "464ae0039a4147", + "bidderRequestId": "3a7736f5f19f638", + "auctionId": "586233c7-4e5d-4231-9f06-b1ff37b0db53", + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 22, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + }, + { + "bidder": "defineMedia", + "params": { + "supplierDomainName": "traffective.com", + "devMode": false + }, + "mediaTypes": { + "banner": { + "sizes": [ + [ + 300, + 250 + ], + [ + 1, + 1 + ] + ] + } + }, + "adUnitCode": "custim-adunit-code-2", + "transactionId": "3f7fa504-f29f-49cc-8edb-31f8b404e27f", + "adUnitId": "1e5fdfe3-b5c7-4dd4-83d1-770bce897773", + "sizes": [ + [ + 300, + 250 + ], + [ + 1, + 1 + ] + ], + "bidId": "53836dbf7d7aac8", + "bidderRequestId": "3a7736f5f19f638", + "auctionId": "586233c7-4e5d-4231-9f06-b1ff37b0db53", + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 19, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + } + ] + + const mockBidderRequest = { + "bidderCode": "defineMedia", + "auctionId": "586233c7-4e5d-4231-9f06-b1ff37b0db53", + "bidderRequestId": "3a7736f5f19f638", + "bids": mockValidBids, + "auctionStart": 1753448647982, + "timeout": 1500, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "topmostLocation": "https://www.any-random-page.com/", + "location": "https://www.any-random-page.com/", + "canonicalUrl": "https://www.any-random-page.com/", + "page": "https://www.any-random-page.com/", + "domain": "www.any-random-page.com", + "ref": "https://www.any-random-page.com/", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "https://www.any-random-page.com/", + "canonicalUrl": "https://www.any-random-page.com/" + } + }, + "ortb2": { + "source": {}, + "site": { + "domain": "any-random-page.com", + "publisher": { + "domain": "any-random-page.com" + }, + "page": "https://www.any-random-page.com/", + "ref": "https://www.any-random-page.com/", + "content": { + "data": [ + { + "name": "articlegenius.ai", + "ext": { + "segtax": 7 + }, + "segment": [ + { + "id": "186" + }, + { + "id": "387" + }, + { + "id": "400" + } + ] + } + ], + "language": "de" + }, + "ext": { + "data": { + "pagetype": "article", + "category": "localnews", + "documentLang": "de", + "adg_rtd": { + "uid": "c7910f59-446a-4786-8826-8181e884afd6", + "pageviewId": "915818a5-73f2-4efb-8eff-dd312755dd4a", + "features": { + "page_dimensions": "1235x6597", + "viewport_dimensions": "1250x959", + "user_timestamp": "1753455847", + "dom_loading": "204" + }, + "session": { + "rnd": 0.8341928086704196, + "pages": 4, + "new": false, + "vwSmplg": 0.1, + "vwSmplgNxt": 0.05, + "expiry": 1753450360922, + "lastActivityTime": 1753448560922, + "id": "bd8b3c7a-ff7f-4433-a5fd-a06cf0fa6e1f" + } + } + } + } + }, + "regs": { + "ext": { + "gdpr": 1 + } + }, + "user": { + "ext": { + "consent": "RANDOMCONSENTSTRING", + "eids": [ + { + "source": "criteo.com", + "uids": [ + { + "id": "random-id", + "atype": 1 + } + ] + } + ] + } + }, + "ext": { + "prebid": { + "adServerCurrency": "EUR" + } + }, + "device": { + "w": 1920, + "h": 1080, + "dnt": 0, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + "language": "de", + "ext": { + "vpw": 1250, + "vph": 959 + }, + "sua": { + "source": 1, + "platform": { + "brand": "Linux" + }, + "browsers": [ + { + "brand": "Google Chrome", + "version": [ + "137" + ] + }, + { + "brand": "Chromium", + "version": [ + "137" + ] + }, + { + "brand": "Not/A)Brand", + "version": [ + "24" + ] + } + ], + "mobile": 0 + } + } + }, + "gdprConsent": { + "consentString": "RANDOMCONSENTSTRING", + "vendorData": { + "cmpId": 21, + "cmpVersion": 2, + "gdprApplies": true, + "tcfPolicyVersion": 5, + "tcString": "RANDOMTCSTRING", + "listenerId": 12, + "eventStatus": "tcloaded", + "cmpStatus": "loaded", + "isServiceSpecific": true, + "useNonStandardTexts": false, + "publisherCC": "DE", + "purposeOneTreatment": false, + "outOfBand": { + "allowedVendors": {}, + "disclosedVendors": {} + }, + "purpose": { + "consents": { + "1": true, + "2": true, + "3": true, + "4": true, + "5": true, + "6": true, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + "legitimateInterests": { + "1": false, + "2": true, + "3": false, + "4": false, + "5": false, + "6": false, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + } + }, + "vendor": { + "consents": { + "755": true, + }, + "legitimateInterests": { + "755": true, + } + }, + "specialFeatureOptins": { + "1": true, + "2": true + }, + "publisher": { + "consents": {}, + "legitimateInterests": {}, + "customPurpose": { + "consents": {}, + "legitimateInterests": {} + }, + "restrictions": {} + }, + "opencmp": { + "consentType": "tcf", + "googleConsent": { + "ad_storage": "granted", + "ad_user_data": "granted", + "ad_personalization": "granted", + "analytics_storage": "granted" + } + }, + "addtlConsent": "2~89~dv.", + "customVendors": { + "consents": { + "45": true + }, + "legitimateInterests": { + "45": false + } + } + }, + "gdprApplies": true, + "apiVersion": 2, + "addtlConsent": "2~89~dv." + }, + "start": 1753448648053 + } + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + for (const bidRequest of mockValidBids) { + assert.isTrue(spec.isBidRequestValid(bidRequest)); + } + }); + + it('should return false when supplierDomainName is not set', function () { + let invalidBids = deepClone(mockValidBids); + for (const bidRequest of invalidBids) { + bidRequest.params = {}; + assert.isFalse(spec.isBidRequestValid(bidRequest)); + } + }); + }); + + describe('buildRequests', function () { + it('should send request with correct structure', function () { + let requests = spec.buildRequests(mockValidBids, mockBidderRequest); + for (const request of requests) { + assert.equal(request.method, 'POST'); + assert.ok(request.data); + } + }); + + it('should have default request structure', function () { + let keys = 'id,imp,site,source,device'.split(','); + let requests = spec.buildRequests(mockValidBids, mockBidderRequest); + + for (const request of requests) { + let data = Object.keys(request.data); + assert.includeDeepMembers(data, keys); + } + }); + + it('Verify the site url', function () { + let siteUrl = 'https://www.yourdomain.tld/your-directory/'; + let bidderRequest = deepClone(mockBidderRequest); + + bidderRequest.ortb2.site.page = siteUrl; + bidderRequest.refererInfo.page = siteUrl; + + let requests = spec.buildRequests(mockValidBids, bidderRequest); + + for (const request of requests) { + assert.equal(request.data.site.page, siteUrl); + } + }); + }); +}) + +describe('interpretResponse', function () { + const formerBids = [ + { + "bidder": "defineMedia", + "params": { + "supplierDomainName": "traffective.com", + "devMode": false + }, + "mediaTypes": { + "banner": { + "sizes": [ + [ + 1, + 1 + ], + [ + 300, + 250 + ] + ] + }, + }, + "adUnitCode": "custom-adunit-code", + "transactionId": null, + "adUnitId": "1eb2de11-c637-4175-b560-002fc4160841", + "sizes": [ + [ + 1, + 1 + ], + [ + 300, + 250 + ] + ], + "bidId": "8566b4bbc519b58", + "bidderRequestId": "7a7870d573e715", + "auctionId": null, + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 22, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + "ortb2": { + "source": {}, + "site": { + "domain": "any-random-page.com", + "publisher": { + "domain": "any-random-page.com" + }, + "page": "https://www.any-random-page.com/", + "ref": "https://www.any-random-page.com/", + "content": { + "data": [ + { + "name": "articlegenius.ai", + "ext": { + "segtax": 7 + }, + "segment": [ + { + "id": "186" + }, + { + "id": "387" + }, + { + "id": "400" + } + ] + } + ], + "language": "de" + }, + "ext": { + "data": { + "pagetype": "article", + "category": "localnews", + "documentLang": "de" + } + } + }, + "device": { + "w": 1920, + "h": 1080, + "dnt": 0, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + "language": "de", + "ext": { + "vpw": 1250, + "vph": 959 + }, + "sua": { + "source": 1, + "platform": { + "brand": "Linux" + }, + "browsers": [ + { + "brand": "Google Chrome", + "version": [ + "137" + ] + }, + { + "brand": "Chromium", + "version": [ + "137" + ] + }, + { + "brand": "Not/A)Brand", + "version": [ + "24" + ] + } + ], + "mobile": 0 + } + } + } + } + ] + const formerBidRequest = { + "bidderCode": "defineMedia", + "auctionId": "0720d855-f13d-41b7-b5cf-41d6c89454af", + "bidderRequestId": "7a7870d573e715", + "bids": formerBids, + "auctionStart": 1753451739223, + "timeout": 1500, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [ + "https://www.any-random-page.com/" + ], + "topmostLocation": "https://www.any-random-page.com/", + "location": "https://www.any-random-page.com/", + "canonicalUrl": "https://www.any-random-page.com/", + "page": "https://www.any-random-page.com/", + "domain": "www.any-random-page.com", + "ref": "https://www.any-random-page.com/", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [ + "https://www.any-random-page.com/" + ], + "referer": "https://www.any-random-page.com/", + "canonicalUrl": "https://www.any-random-page.com" + } + }, + "ortb2": { + "source": {}, + "site": { + "domain": "any-random-page.com", + "publisher": { + "domain": "any-random-page.com" + }, + "page": "https://www.any-random-page.com/", + "ref": "https://www.any-random-page.com/", + "content": { + "data": [ + { + "name": "articlegenius.ai", + "ext": { + "segtax": 7 + }, + "segment": [ + { + "id": "186" + }, + { + "id": "387" + }, + { + "id": "400" + } + ] + } + ], + "language": "de" + }, + "ext": { + "data": { + "pagetype": "article", + "category": "localnews", + "documentLang": "de" + } + } + }, + "device": { + "w": 1920, + "h": 1080, + "dnt": 0, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + "language": "de", + "ext": { + "vpw": 1250, + "vph": 959 + }, + "sua": { + "source": 1, + "platform": { + "brand": "Linux" + }, + "browsers": [ + { + "brand": "Google Chrome", + "version": [ + "137" + ] + }, + { + "brand": "Chromium", + "version": [ + "137" + ] + }, + { + "brand": "Not/A)Brand", + "version": [ + "24" + ] + } + ], + "mobile": 0 + } + } + }, + "start": 1753451739307 + } + + const goodBannerRequest = { + "imp": [ + { + "id": "8566b4bbc519b58", + "banner": { + "topframe": 0, + "format": [ + { + "w": 1, + "h": 1 + }, + { + "w": 300, + "h": 250 + } + ] + }, + "secure": 1, + } + ], + "source": { + "schain": { + "complete": 1, + "nodes": [ + { + "asi": "traffective.com" + } + ] + } + }, + "site": { + "domain": "any-random-page.com", + "publisher": { + "domain": "any-random-page.com" + }, + "page": "https://www.any-random-page.com/", + "ref": "https://www.any-random-page.com/", + "content": { + "data": [ + { + "name": "articlegenius.ai", + "ext": { + "segtax": 7 + }, + "segment": [ + { + "id": "186" + }, + { + "id": "387" + }, + { + "id": "400" + } + ] + } + ], + "language": "de" + }, + "ext": { + "data": { + "pagetype": "article", + "category": "localnews", + "documentLang": "de" + } + } + }, + "device": { + "w": 1920, + "h": 1080, + "dnt": 0, + "ua": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + "language": "de", + "ext": { + "vpw": 1250, + "vph": 959 + }, + "sua": { + "source": 1, + "platform": { + "brand": "Linux" + }, + "browsers": [ + { + "brand": "Google Chrome", + "version": [ + "137" + ] + }, + { + "brand": "Chromium", + "version": [ + "137" + ] + }, + { + "brand": "Not/A)Brand", + "version": [ + "24" + ] + } + ], + "mobile": 0 + } + }, + // "id": "15009fd8-a057-458f-9819-5ddcbf474cfe", //this is a random uuid, so it is not set here + "test": 0, + "tmax": 1500 + } + const goodBannerResponse = { + // "id": "15009fd8-a057-458f-9819-5ddcbf474cfe", //this is a random uuid, so it is not set here + "seatbid": [ + { + "bid": [ + { + "id": "23da7c99-8945-4ff9-6426-3da72d25e73a", + "impid": "8566b4bbc519b58", + "price": 1.0, + "burl": "https://somewhere-in-the-internet.com", + "lurl": "https://somewhere-in-the-internet.com", + "adm": "

ad markup

", + "adid": "e44efd3c-0b58-4834-8fc0-d9d3f658fa1c", + "adomain": [ + "definemedia.de" + ], + "crid": "dim_playout$6b3082ae93341939", + "w": 800, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "definemedia" + } + ], + "cur": "EUR" + } + const goodInterpretedBannerResponses = [ + { + "mediaType": "banner", + "ad": "

ad markup

", + "requestId": "8566b4bbc519b58", + "seatBidId": "23da7c99-8945-4ff9-6426-3da72d25e73a", + "cpm": 1.0, + "currency": "EUR", + "width": 800, + "height": 250, + "creative_id": "dim_playout$6b3082ae93341939", + "creativeId": "dim_playout$6b3082ae93341939", + "burl": "https://somewhere-in-the-internet.com", + "ttl": 1000, + "netRevenue": true, + "meta": { "advertiserDomains": ["definemedia.de"] } + } + ] + it('should return null if body is missing or empty', function () { + let serverResponse = { + body: null + } + let request = { + data: deepClone(goodBannerRequest) + } + + const result = spec.interpretResponse(serverResponse, request); + assert.equal(result.length, 0); + }); + + it('should return the correct params', function () { + const computedRequest = spec.buildRequests(formerBids, formerBidRequest)[0] + let computedRequestExpected = deepClone(computedRequest.data); + assert.deepInclude(computedRequestExpected, goodBannerRequest) + let serverResponse = { + body: deepClone(goodBannerResponse) + } + const result = spec.interpretResponse(serverResponse, computedRequest); + assert.notEqual(result, null); + + const bid = result[0].cpm + assert.isAbove(bid, 0.01, "Bid price should be higher 0.0"); + assert.deepInclude(result[0], goodInterpretedBannerResponses[0]) + }); +}) diff --git a/test/spec/modules/empowerBidAdapter_spec.js b/test/spec/modules/empowerBidAdapter_spec.js new file mode 100644 index 00000000000..b260d112499 --- /dev/null +++ b/test/spec/modules/empowerBidAdapter_spec.js @@ -0,0 +1,798 @@ +import { expect } from "chai"; +import { spec, ENDPOINT } from "modules/empowerBidAdapter.js"; +import { config } from "src/config.js"; +import { setConfig as setCurrencyConfig } from "../../../modules/currency.js"; +import * as utils from "src/utils.js"; + +describe("EmpowerAdapter", function () { + let baseBidRequest; + + let bannerBidRequest; + let bannerServerResponse; + let bannerServerRequest; + + let videoBidRequest; + let videoServerResponse; + let videoServerRequest; + + let bidderRequest; + + beforeEach(function () { + bidderRequest = { + refererInfo: { + page: "https://publisher.com/home", + domain: "publisher.com", + }, + }; + + baseBidRequest = { + bidder: "empower", + params: { + zone: "123456", + }, + bidId: "2ffb201a808da7", + bidderRequestId: "678e3fbad375ce", + auctionId: "c45dd708-a418-42ec-b8a7-b70a6c6fab0a", + transactionId: "d45dd707-a418-42ec-b8a7-b70a6c6fab0b", + }; + + bannerBidRequest = { + ...baseBidRequest, + mediaTypes: { + banner: { + sizes: [ + [970, 250], + [300, 250], + ], + }, + }, + sizes: [ + [640, 320], + [300, 600], + ], + }; + + bannerServerResponse = { + id: "678e3fbad375ce", + cur: "USD", + seatbid: [ + { + bid: [ + { + id: "288f5e3e-f122-4928-b5df-434f5b664788", + impid: "2ffb201a808da7", + price: 0.12, + cid: "12", + crid: "123", + adomain: ["empower.net"], + adm: '', + burl: "https://localhost:8081/url/b?d=b604923d-f420-4227-a8af-09b332b33c2d&c=USD&p=${AUCTION_PRICE}&bad=33d141da-dd49-45fc-b29d-1ed38a2168df&gc=0", + nurl: "https://ng.virgul.com/i_wu?a=fac123456&ext=,ap0.12,acUSD,sp0.1,scUSD", + w: 640, + h: 360, + }, + ], + }, + ], + }; + + bannerServerRequest = { + method: "POST", + url: "https://bid.virgul.com/prebid", + data: JSON.stringify({ + id: "678e3fbad375ce", + imp: [ + { + id: "2ffb201a808da7", + bidfloor: 5, + bidfloorcur: "USD", + tagid: "123456", + banner: { + w: 640, + h: 360, + format: [ + { w: 640, h: 360 }, + { w: 320, h: 320 }, + ], + }, + }, + ], + site: { + publisher: { + id: "44bd6161-667e-4a68-8fa4-18b5ae2d8c89", + }, + id: "1d973061-fe5d-4622-a071-d8a01d72ba7d", + ref: "", + page: "http://localhost", + domain: "localhost", + }, + app: null, + device: { + ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36", + language: "en-US", + }, + isPrebid: true, + }), + }; + + videoBidRequest = { + ...baseBidRequest, + mediaTypes: { video: { playerSize: [[640, 360]] } }, + }; + + videoServerResponse = { + id: "678e3fbad375ce", + cur: "USD", + seatbid: [ + { + bid: [ + { + id: "288f5e3e-f122-4928-b5df-434f5b664788", + impid: "2ffb201a808da7", + price: 0.12, + cid: "12", + crid: "123", + adomain: ["empower.net"], + adm: "", + burl: "https://localhost:8081/url/b?d=b604923d-f420-4227-a8af-09b332b33c2d&c=USD&p=${AUCTION_PRICE}&bad=33d141da-dd49-45fc-b29d-1ed38a2168df&gc=0", + nurl: "https://ng.virgul.com/i_wu?a=fac123456&ext=,ap01.12,acUSD,sp0.1,scUSD", + w: 640, + h: 360, + }, + ], + }, + ], + }; + + videoServerRequest = { + method: "POST", + url: "https://bid.virgul.com/prebid", + data: JSON.stringify({ + id: "678e3fbad375ce", + imp: [ + { + id: "2ffb201a808da7", + bidfloor: 5, + bidfloorcur: "USD", + tagid: "123456", + video: { playerSize: [[640, 360]] }, + }, + ], + site: { + publisher: { + id: "44bd6161-667e-4a68-8fa4-18b5ae2d8c89", + }, + id: "1d973061-fe5d-4622-a071-d8a01d72ba7d", + ref: "", + page: "http://localhost", + domain: "localhost", + }, + app: null, + device: { + ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36", + language: "en-US", + }, + isPrebid: true, + }), + }; + }); + + describe("Banner", function () { + describe("spec.isBidRequestValid", function () { + it("should return true when the required params are passed to banner", function () { + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true); + }); + + it('should return false when the "zone" param is missing for banner', function () { + bannerBidRequest.params = { + bidfloor: 5.0, + }; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); + }); + + it("should return false when no bid params are passed to banner", function () { + bannerBidRequest.params = {}; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); + }); + }); + + describe("spec.buildRequests", function () { + it("should create a POST request for every bid", function () { + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + expect(request.method).to.equal("POST"); + expect(request.url).to.equal(ENDPOINT); + }); + + it("should attach request data to banner", function () { + config.setConfig({ + currency: { + adServerCurrency: "EUR", + }, + }); + + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal("$prebid.version$"); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(0); + expect(data.imp[0].bidfloorcur).to.equal("EUR"); + expect(data.imp[0].tagid).to.equal("123456"); + expect(data.imp[0].ext.zone).to.equal(bannerBidRequest.params.zone); + expect(data.site.page).to.equal(bidderRequest.refererInfo.page); + expect(data.site.domain).to.equal(bidderRequest.refererInfo.domain); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language, + }); + expect(data.cur).to.deep.equal(["EUR"]); + }); + + describe("spec.interpretResponse", function () { + it("should return no bids if the response is invalid request", function () { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + {} + ); + expect(bidResponse.length).to.equal(0); + }); + + it("should return no bids if the response is invalid body json", function () { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + { data: "invalid body " } + ); + expect(bidResponse.length).to.equal(0); + }); + + it("should return a valid bid a valid body", function () { + bannerServerRequest.data = JSON.parse(bannerServerRequest.data); + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + bannerServerRequest + ); + expect(bidResponse.length).to.equal(1); + }); + + it("should return no bids if the response is not valid to banner", function () { + const bidResponse = spec.interpretResponse( + { body: null }, + bannerServerRequest + ); + expect(bidResponse.length).to.equal(0); + }); + + it("should return a valid bid response to banner", function () { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + bannerServerRequest + )[0]; + + expect(bidResponse).to.contain({ + requestId: bannerBidRequest.bidId, + cpm: bannerServerResponse.seatbid[0].bid[0].price, + creativeId: bannerServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: "banner", + currency: bannerServerResponse.cur, + ad: bannerServerResponse.seatbid[0].bid[0].adm, + width: bannerServerResponse.seatbid[0].bid[0].w, + height: bannerServerResponse.seatbid[0].bid[0].h, + burl: bannerServerResponse.seatbid[0].bid[0].burl, + nurl: bannerServerResponse.seatbid[0].bid[0].nurl, + }); + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ["empower.net"], + }); + }); + }); + }); + + describe("Video", function () { + describe("spec.isBidRequestValid", function () { + it("should return true when the required params are passed", function () { + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true); + }); + + it('should return false when the "zone" param is missing', function () { + videoBidRequest.params = { + bidfloor: 5.0, + }; + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(false); + }); + + it("should return false when no bid params are passed", function () { + videoBidRequest.params = {}; + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(false); + }); + }); + + describe("spec.buildRequests", function () { + it("should create a POST request for every bid", function () { + const request = spec.buildRequests([videoBidRequest], bidderRequest); + expect(request.method).to.equal("POST"); + expect(request.url).to.equal(ENDPOINT); + }); + + it("should attach request data to video", function () { + config.setConfig({ + currency: { + adServerCurrency: "EUR", + }, + }); + + const request = spec.buildRequests([videoBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal("$prebid.version$"); + expect(data.id).to.equal(videoBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(0); + expect(data.imp[0].bidfloorcur).to.equal("EUR"); + expect(data.imp[0].tagid).to.equal("123456"); + + expect(data.imp[0].ext.zone).to.equal(videoBidRequest.params.zone); + expect(data.site.page).to.equal(bidderRequest.refererInfo.page); + expect(data.site.domain).to.equal(bidderRequest.refererInfo.domain); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language, + }); + expect(data.cur).to.deep.equal(["EUR"]); + }); + + it("should get bid floor from module", function () { + const floorModuleData = { + currency: "USD", + floor: 3.2, + }; + videoBidRequest.getFloor = function () { + return floorModuleData; + }; + const request = spec.buildRequests([videoBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal("$prebid.version$"); + expect(data.id).to.equal(videoBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); + }); + + it("should send gdpr data when gdpr does not apply", function () { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + }, + }; + const request = spec.buildRequests([videoBidRequest], { + ...bidderRequest, + ...gdprData, + }); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: "", + }, + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false, + }, + }); + }); + + it("should send gdpr data when gdpr applies", function () { + const tcString = "sometcstring"; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString, + }, + }; + const request = spec.buildRequests([videoBidRequest], { + ...bidderRequest, + ...gdprData, + }); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: tcString, + }, + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true, + }, + }); + }); + }); + + describe("spec.interpretResponse", function () { + it("should return no bids if the response is not valid", function () { + const bidResponse = spec.interpretResponse( + { body: null }, + videoServerRequest + ); + expect(bidResponse.length).to.equal(0); + }); + + it("should return a valid bid response to video", function () { + const bidResponse = spec.interpretResponse( + { body: videoServerResponse }, + videoServerRequest + )[0]; + + expect(bidResponse).to.contain({ + requestId: videoBidRequest.bidId, + cpm: videoServerResponse.seatbid[0].bid[0].price, + creativeId: videoServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: "video", + currency: videoServerResponse.cur, + vastXml: videoServerResponse.seatbid[0].bid[0].adm, + }); + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ["empower.net"], + }); + }); + }); + }); + + describe("Modules", function () { + it("should attach user Ids", function () { + const userIdAsEids = { + userIdAsEids: [ + { + source: "pubcid.org", + uids: [ + { + id: "abcxyzt", + atype: 1, + }, + ], + }, + { + source: "criteo.com", + uids: [ + { + id: "qwertyu", + atype: 1, + }, + ], + }, + ], + }; + bannerBidRequest = { ...bannerBidRequest, ...userIdAsEids }; + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + const data = JSON.parse(request.data); + + expect(data.user.eids.length).to.equal(2); + expect(data.user.eids[0].source).to.equal("pubcid.org"); + expect(data.user.eids[1].uids.length).to.equal(1); + expect(data.user.eids[1].uids[0].id).to.equal("qwertyu"); + }); + + it("should get bid floor from module", function () { + const floorModuleData = { + currency: "USD", + floor: 3.2, + }; + bannerBidRequest.getFloor = function () { + return floorModuleData; + }; + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal("$prebid.version$"); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); + }); + + it("should send gdpr data when gdpr does not apply", function () { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...gdprData, + }); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: "", + }, + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false, + }, + }); + }); + + it("should send gdpr data when gdpr applies", function () { + const tcString = "sometcstring"; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...gdprData, + }); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: tcString, + }, + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true, + }, + }); + }); + }); + describe("Ortb2", function () { + it("should attach schain", function () { + const schain = { + ortb2: { + source: { + ext: { + schain: { + ver: "1.0", + complete: 1, + nodes: [ + { + asi: "empower.net", + sid: "111222333", + hp: 1, + }, + ], + }, + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...schain, + }); + const data = JSON.parse(request.data); + expect(data.schain.ver).to.equal("1.0"); + expect(data.schain.nodes.length).to.equal(1); + expect(data.schain.nodes[0].sid).to.equal("111222333"); + expect(data.schain.nodes[0].asi).to.equal("empower.net"); + }); + + it("should attach badv", function () { + const badv = { + ortb2: { badv: ["bad.example.com"] }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...badv, + }); + const data = JSON.parse(request.data); + expect(data.badv.length).to.equal(1); + expect(data.badv[0]).to.equal("bad.example.com"); + }); + + it("should attach bcat", function () { + const bcat = { + ortb2: { bcat: ["IAB-1-2", "IAB-1-2"] }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...bcat, + }); + const data = JSON.parse(request.data); + expect(data.bcat.length).to.equal(2); + expect(data.bcat).to.deep.equal(bcat.ortb2.bcat); + }); + + it("should override initial device", function () { + const device = { + ortb2: { + device: { + w: 390, + h: 844, + dnt: 0, + ua: "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1", + language: "en", + ext: { + vpw: 390, + vph: 844, + }, + sua: { + source: 1, + browsers: [], + mobile: 1, + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...device, + }); + const data = JSON.parse(request.data); + expect(data.device.ua).to.equal(device.ortb2.device.ua); + expect(data.device.sua.mobile).to.equal(device.ortb2.device.sua.mobile); + }); + + it("should override initial site", function () { + const site = { + ortb2: { + site: { + publisher: { + domain: "empower.net", + }, + page: "https://empower.net/prebid", + name: "empower.net", + cat: [], + sectioncat: [], + pagecat: [], + ref: "", + ext: { + data: {}, + }, + content: { + language: "en", + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...site, + }); + const data = JSON.parse(request.data); + expect(data.site.page).to.equal(site.ortb2.site.page); + expect(data.site.domain).to.equal("publisher.com"); + }); + + it("should attach device and user geo via device", function () { + const device = { + ortb2: { + device: { + geo: { + lat: 1, + lon: -1, + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...device, + }); + const data = JSON.parse(request.data); + expect(data.device.geo.lat).to.equal(device.ortb2.device.geo.lat); + expect(data.user.geo.lat).to.equal(device.ortb2.device.geo.lat); + }); + + it("should attach device and user geo via user", function () { + const ortb2 = { + ortb2: { + user: { + geo: { + lat: 1, + lon: -1, + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...ortb2, + }); + const data = JSON.parse(request.data); + expect(data.device.geo.lat).to.equal(ortb2.ortb2.user.geo.lat); + expect(data.user.geo.lat).to.equal(ortb2.ortb2.user.geo.lat); + }); + + it("should attach device and user geo both device/user", function () { + const ortb2 = { + ortb2: { + user: { + geo: { + lat: 1, + lon: -1, + }, + }, + device: { + geo: { + lat: 2, + lon: -1, + }, + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...ortb2, + }); + const data = JSON.parse(request.data); + expect(data.device.geo.lat).to.equal(ortb2.ortb2.device.geo.lat); + expect(data.user.geo.lat).to.equal(ortb2.ortb2.user.geo.lat); + }); + + it("should override initial user", function () { + const user = { + ortb2: { + user: { + gender: "F", + }, + }, + }; + const request = spec.buildRequests([bannerBidRequest], { + ...bidderRequest, + ...user, + }); + const data = JSON.parse(request.data); + expect(data.user.gender).to.equal(user.ortb2.user.gender); + }); + }); + }); + + describe("onBidWon", function () { + beforeEach(function () { + sinon.stub(utils, "triggerPixel"); + }); + afterEach(function () { + utils.triggerPixel.restore(); + }); + + it("Should not trigger pixel if bid does not contain nurl", function () { + spec.onBidWon({}); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it("Should not trigger pixel if nurl is empty", function () { + spec.onBidWon({ + nurl: "", + }); + + expect(utils.triggerPixel.called).to.be.false; + }); + + it("Should trigger pixel with replaced nurl if nurl is not empty", function () { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + bannerServerRequest + ); + const bidToWon = bidResponse[0]; + bidToWon.adserverTargeting = { + hb_pb: 0.1, + }; + spec.onBidWon(bidToWon); + + expect(utils.triggerPixel.callCount).to.be.equal(1); + expect(utils.triggerPixel.firstCall.args[0]).to.be.equal( + "https://ng.virgul.com/i_wu?a=fac123456&ext=,ap0.12,acUSD,sp0.1,scUSD" + ); + setCurrencyConfig({}); + }); + }); +}); diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index eaf5b5f40c2..ed9d7b1639b 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -75,12 +75,13 @@ describe('finteza analytics adapter', function () { // Emit the events with the "real" arguments events.emit(EVENTS.BID_REQUESTED, bidRequest); - expect(server.requests.length).to.equal(1); + const reqs = server.requests.filter(req => req.url.startsWith('https://content.mql5.com')); + expect(reqs.length).to.eql(1); - expect(server.requests[0].method).to.equal('GET'); - expect(server.requests[0].withCredentials).to.equal(true); + expect(reqs[0].method).to.equal('GET'); + expect(reqs[0].withCredentials).to.equal(true); - const url = parseUrl(server.requests[0].url); + const url = parseUrl(reqs[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -88,8 +89,9 @@ describe('finteza analytics adapter', function () { expect(url.search.id).to.equal(clientId); expect(url.search.fz_uniq).to.equal(uniqCookie); expect(decodeURIComponent(url.search.event)).to.equal(`Bid Request ${bidderCode.toUpperCase()}`); - - sinon.assert.callCount(fntzAnalyticsAdapter.track, 1); + sinon.assert.calledWith(fntzAnalyticsAdapter.track, sinon.match({ + eventType: EVENTS.BID_REQUESTED + })); }); }); @@ -117,13 +119,12 @@ describe('finteza analytics adapter', function () { // Emit the events with the "real" arguments events.emit(EVENTS.BID_RESPONSE, bidResponse); + const reqs = server.requests.filter(req => req.url.startsWith('https://content.mql5.com')); + expect(reqs.length).to.equal(2); + expect(reqs[0].method).to.equal('GET'); + expect(reqs[0].withCredentials).to.equal(true); - expect(server.requests[0].method).to.equal('GET'); - expect(server.requests[0].withCredentials).to.equal(true); - - expect(server.requests.length).to.equal(2); - - let url = parseUrl(server.requests[0].url); + let url = parseUrl(reqs[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -134,10 +135,10 @@ describe('finteza analytics adapter', function () { expect(url.search.value).to.equal(String(cpm)); expect(url.search.unit).to.equal('usd'); - expect(server.requests[1].method).to.equal('GET'); - expect(server.requests[1].withCredentials).to.equal(true); + expect(reqs[1].method).to.equal('GET'); + expect(reqs[1].withCredentials).to.equal(true); - url = parseUrl(server.requests[1].url); + url = parseUrl(reqs[1].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -148,7 +149,9 @@ describe('finteza analytics adapter', function () { expect(url.search.value).to.equal(String(timeToRespond)); expect(url.search.unit).to.equal('ms'); - sinon.assert.callCount(fntzAnalyticsAdapter.track, 1); + sinon.assert.calledWith(fntzAnalyticsAdapter.track, sinon.match({ + eventType: EVENTS.BID_RESPONSE + })); }); }); @@ -172,12 +175,13 @@ describe('finteza analytics adapter', function () { // Emit the events with the "real" arguments events.emit(EVENTS.BID_WON, bidWon); - expect(server.requests[0].method).to.equal('GET'); - expect(server.requests[0].withCredentials).to.equal(true); + const reqs = server.requests.filter((req) => req.url.startsWith('https://content.mql5.com')); + expect(reqs[0].method).to.equal('GET'); + expect(reqs[0].withCredentials).to.equal(true); - expect(server.requests.length).to.equal(1); + expect(reqs.length).to.equal(1); - const url = parseUrl(server.requests[0].url); + const url = parseUrl(reqs[0].url); expect(url.protocol).to.equal('https'); expect(url.hostname).to.equal('content.mql5.com'); @@ -188,8 +192,7 @@ describe('finteza analytics adapter', function () { expect(url.search.value).to.equal(String(cpm)); expect(url.search.unit).to.equal('usd'); - // 1 Finteza event + 1 Clean.io event - sinon.assert.callCount(fntzAnalyticsAdapter.track, 2); + sinon.assert.calledWith(fntzAnalyticsAdapter.track, sinon.match({eventType: EVENTS.BID_WON})) }); }); diff --git a/test/spec/modules/fwsspBidAdapter_spec.js b/test/spec/modules/fwsspBidAdapter_spec.js index cf43712c6c1..ee7e762c18c 100644 --- a/test/spec/modules/fwsspBidAdapter_spec.js +++ b/test/spec/modules/fwsspBidAdapter_spec.js @@ -10,7 +10,6 @@ describe('fwsspBidAdapter', () => { networkId: '42015', profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', - videoAssetId: '0' } }; expect(spec.isBidRequestValid(bid)).to.be.true; @@ -22,7 +21,6 @@ describe('fwsspBidAdapter', () => { networkId: '42015', profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', - videoAssetId: '0' } }; expect(spec.isBidRequestValid(bid)).to.be.false; @@ -34,7 +32,6 @@ describe('fwsspBidAdapter', () => { serverUrl: 'https://example.com/ad/g/1', profile: '42015:js_allinone_profile', siteSectionId: 'js_allinone_demo_site_section', - videoAssetId: '0' } }; expect(spec.isBidRequestValid(bid)).to.be.false; @@ -46,7 +43,6 @@ describe('fwsspBidAdapter', () => { serverUrl: 'https://example.com/ad/g/1', networkId: '42015', siteSectionId: 'js_allinone_demo_site_section', - videoAssetId: '0' } }; expect(spec.isBidRequestValid(bid)).to.be.false; @@ -58,19 +54,6 @@ describe('fwsspBidAdapter', () => { serverUrl: 'https://example.com/ad/g/1', networkId: '42015', profile: '42015:js_allinone_profile', - videoAssetId: '0' - } - }; - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - - it('should return false when videoAssetId is missing', () => { - const bid = { - params: { - serverUrl: 'https://example.com/ad/g/1', - networkId: '42015', - profile: '42015:js_allinone_profile', - siteSectionId: 'js_allinone_demo_site_section' } }; expect(spec.isBidRequestValid(bid)).to.be.false; @@ -112,7 +95,6 @@ describe('fwsspBidAdapter', () => { 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', 'flags': '+play', - 'videoAssetId': '0', 'timePosition': 120, 'adRequestKeyValues': { '_fw_player_width': '1920', @@ -137,7 +119,7 @@ describe('fwsspBidAdapter', () => { } }; - it('should build a valid server request', () => { + it('should build a valid server request with default caid of 0', () => { const requests = spec.buildRequests(getBidRequests(), bidderRequest); expect(requests).to.be.an('array').that.is.not.empty; const request = requests[0]; @@ -169,7 +151,7 @@ describe('fwsspBidAdapter', () => { expect(actualDataString).to.not.include('mind'); expect(actualDataString).to.not.include('maxd;'); // schain check - const expectedEncodedSchainString = encodeURIComponent('{"ver":"1.0","complete":1,"nodes":[{"asi":"example.com","sid":"0","hp":1,"rid":"bidrequestid","domain":"example.com"}]}'); + const expectedEncodedSchainString = '1.0,1!example.com,0,1,bidrequestid,,example.com'; expect(actualDataString).to.include(expectedEncodedSchainString); }); @@ -177,13 +159,23 @@ describe('fwsspBidAdapter', () => { const requests = spec.buildRequests(getBidRequests(), bidderRequest); expect(requests).to.be.an('array').that.is.not.empty; const request = requests[0]; - const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=on-demand&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=EUR&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22example.com%22%2C%22sid%22%3A%220%22%2C%22hp%22%3A1%2C%22rid%22%3A%22bidrequestid%22%2C%22domain%22%3A%22example.com%22%7D%5D%7D;tpos=0&ptgt=a&slid=Preroll_1&slau=preroll;`; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=on-demand&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=EUR&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&schain=1.0,1!example.com,0,1,bidrequestid,,example.com;tpos=0&ptgt=a&slid=Preroll_1&slau=preroll;`; const actualUrl = `${request.url}?${request.data}`; // Remove pvrn and vprn from both URLs before comparing const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); expect(cleanUrl(actualUrl)).to.equal(cleanUrl(expectedUrl)); }); + it('should use params.videoAssetId as caid', () => { + const bidRequests = getBidRequests(); + bidRequests[0].params.videoAssetId = 10; + const requests = spec.buildRequests(bidRequests, bidderRequest); + expect(requests).to.be.an('array').that.is.not.empty; + const request = requests[0]; + const actualDataString = request.data; + expect(actualDataString).to.include('caid=10'); + }); + it('should return the correct width and height when _fw_player_width and _fw_player_height are not present in adRequestKeyValues', () => { const bidRequests = [{ 'bidder': 'fwssp', @@ -220,7 +212,7 @@ describe('fwsspBidAdapter', () => { const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest.gdprConsent, null, null); expect(userSyncs).to.deep.equal([{ type: 'image', - url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=consentString' + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&gdpr=1&gdpr_consent=consentString' }]); }); @@ -231,7 +223,64 @@ describe('fwsspBidAdapter', () => { const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest.gdprConsent, bidderRequest.uspConsent, bidderRequest.gppConsent); expect(userSyncs).to.deep.equal([{ type: 'iframe', - url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid[]=8' + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid=8' + }]); + }); + + it('should add privacy values to ad request and user sync url when present in keyValues', () => { + const bidRequests = getBidRequests(); + + bidRequests[0].params.adRequestKeyValues._fw_coppa = 1; + bidRequests[0].params.adRequestKeyValues._fw_atts = 1; + bidRequests[0].params.adRequestKeyValues._fw_is_lat = 1; + const requests = spec.buildRequests(bidRequests, bidderRequest); + const request = requests[0]; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=on-demand&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_coppa=1&_fw_atts=1&_fw_is_lat=1&_fw_bidfloor=2&_fw_bidfloorcur=EUR&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&schain=1.0,1!example.com,0,1,bidrequestid,,example.com;tpos=0&ptgt=a&slid=Preroll_1&slau=preroll;`; + const actualUrl = `${request.url}?${request.data}`; + // Remove pvrn and vprn from both URLs before comparing + const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); + expect(cleanUrl(actualUrl)).to.equal(cleanUrl(expectedUrl)); + + const syncOptions = { + 'iframeEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest.gdprConsent, bidderRequest.uspConsent, bidderRequest.gppConsent); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&_fw_coppa=1&_fw_atts=1&_fw_is_lat=1&gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid=8' + }]); + }); + + it('ortb2 values should take precedence over keyValues when present and be added to ad request and user sync url', () => { + const bidRequests = getBidRequests(); + bidRequests[0].params.adRequestKeyValues._fw_coppa = 1; + bidRequests[0].params.adRequestKeyValues._fw_atts = 1; + bidRequests[0].params.adRequestKeyValues._fw_is_lat = 1; + + const bidderRequest2 = { ...bidderRequest } + bidderRequest2.ortb2 = { + regs: { coppa: 0 }, + device: { + lmt: 0, + ext: { atts: 0 } + } + } + + const requests = spec.buildRequests(bidRequests, bidderRequest2); + const request = requests[0]; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=on-demand&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_coppa=0&_fw_atts=0&_fw_is_lat=0&_fw_bidfloor=2&_fw_bidfloorcur=EUR&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&schain=1.0,1!example.com,0,1,bidrequestid,,example.com;tpos=0&ptgt=a&slid=Preroll_1&slau=preroll;`; + const actualUrl = `${request.url}?${request.data}`; + // Remove pvrn and vprn from both URLs before comparing + const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); + expect(cleanUrl(actualUrl)).to.equal(cleanUrl(expectedUrl)); + + const syncOptions = { + 'iframeEnabled': true + } + const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest2.gdprConsent, bidderRequest2.uspConsent, bidderRequest2.gppConsent); + expect(userSyncs).to.deep.equal([{ + type: 'iframe', + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&_fw_coppa=0&_fw_atts=0&_fw_is_lat=0&gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid=8' }]); }); @@ -274,7 +323,7 @@ describe('fwsspBidAdapter', () => { const request = requests[0]; // schain check - const expectedEncodedSchainString = encodeURIComponent(JSON.stringify(schain1)); + const expectedEncodedSchainString = '1.0,1!test1.com,0,1,bidrequestid1,,test1.com'; expect(request.data).to.include(expectedEncodedSchainString); }); @@ -305,7 +354,7 @@ describe('fwsspBidAdapter', () => { const request = requests[0]; // schain check - const expectedEncodedSchainString = encodeURIComponent(JSON.stringify(schain2)); + const expectedEncodedSchainString = '1.0,1!test2.com,0,2,bidrequestid2,,test2.com'; expect(request.data).to.include(expectedEncodedSchainString); }); }); @@ -344,7 +393,6 @@ describe('fwsspBidAdapter', () => { 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', 'flags': '+play', - 'videoAssetId': '0', 'mode': 'live', 'timePosition': 120, 'tpos': 300, @@ -389,9 +437,9 @@ describe('fwsspBidAdapter', () => { it('should return context and placement with default values', () => { const request = spec.buildRequests(getBidRequests()); const payload = request[0].data; - expect(payload).to.include('_fw_video_context=&'); ; + expect(payload).to.include('_fw_video_context=&'); expect(payload).to.include('_fw_placement_type=null&'); - expect(payload).to.include('_fw_plcmt_type=null;'); + expect(payload).to.include('_fw_plcmt_type=null&'); }); it('should assign placement and context when format is inbanner', () => { @@ -400,9 +448,9 @@ describe('fwsspBidAdapter', () => { bidRequest.mediaTypes.video.plcmt = 'test-plcmt-type'; const request = spec.buildRequests([bidRequest]); const payload = request[0].data; - expect(payload).to.include('_fw_video_context=In-Banner&'); ; + expect(payload).to.include('_fw_video_context=In-Banner&'); expect(payload).to.include('_fw_placement_type=2&'); - expect(payload).to.include('_fw_plcmt_type=test-plcmt-type;'); + expect(payload).to.include('_fw_plcmt_type=test-plcmt-type&'); }); it('should build a valid server request', () => { @@ -440,7 +488,7 @@ describe('fwsspBidAdapter', () => { expect(actualDataString).to.include('mind=30'); expect(actualDataString).to.include('maxd=60;'); // schain check - const expectedEncodedSchainString = encodeURIComponent('{"ver":"1.0","complete":1,"nodes":[{"asi":"example.com","sid":"0","hp":1,"rid":"bidrequestid","domain":"example.com"}]}'); + const expectedEncodedSchainString = '1.0,1!example.com,0,1,bidrequestid,,example.com'; expect(actualDataString).to.include(expectedEncodedSchainString); }); @@ -449,7 +497,7 @@ describe('fwsspBidAdapter', () => { expect(requests).to.be.an('array').that.is.not.empty; const request = requests[0]; - const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=live&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=USD&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_gdpr_consented_providers=test_providers&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&_fw_prebid_content=%7B%22id%22%3A%22test_content_id%22%2C%22title%22%3A%22test_content_title%22%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22example.com%22%2C%22sid%22%3A%220%22%2C%22hp%22%3A1%2C%22rid%22%3A%22bidrequestid%22%2C%22domain%22%3A%22example.com%22%7D%5D%7D&loc=http%3A%2F%2Fwww.test.com&_fw_video_context=&_fw_placement_type=null&_fw_plcmt_type=null;tpos=300&ptgt=a&slid=Midroll&slau=midroll&mind=30&maxd=60;`; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=live&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=USD&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_gdpr_consented_providers=test_providers&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&_fw_prebid_content=%7B%22id%22%3A%22test_content_id%22%2C%22title%22%3A%22test_content_title%22%7D&loc=http%3A%2F%2Fwww.test.com&_fw_video_context=&_fw_placement_type=null&_fw_plcmt_type=null&schain=1.0,1!example.com,0,1,bidrequestid,,example.com;tpos=300&ptgt=a&slid=Midroll&slau=midroll&mind=30&maxd=60;`; const actualUrl = `${request.url}?${request.data}`; // Remove pvrn and vprn from both URLs before comparing const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); @@ -475,7 +523,7 @@ describe('fwsspBidAdapter', () => { const requests = spec.buildRequests(getBidRequests(), bidderRequest2); expect(requests).to.be.an('array').that.is.not.empty; const request = requests[0]; - const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=live&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=USD&_fw_gdpr_consented_providers=test_providers&gpp=test_ortb2_gpp&gpp_sid=test_ortb2_gpp_sid&_fw_prebid_content=%7B%22id%22%3A%22test_content_id%22%2C%22title%22%3A%22test_content_title%22%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22example.com%22%2C%22sid%22%3A%220%22%2C%22hp%22%3A1%2C%22rid%22%3A%22bidrequestid%22%2C%22domain%22%3A%22example.com%22%7D%5D%7D&_fw_video_context=&_fw_placement_type=null&_fw_plcmt_type=null;tpos=300&ptgt=a&slid=Midroll&slau=midroll&mind=30&maxd=60;`; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=live&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=USD&_fw_gdpr_consented_providers=test_providers&gpp=test_ortb2_gpp&gpp_sid=test_ortb2_gpp_sid&_fw_prebid_content=%7B%22id%22%3A%22test_content_id%22%2C%22title%22%3A%22test_content_title%22%7D&_fw_video_context=&_fw_placement_type=null&_fw_plcmt_type=null&schain=1.0,1!example.com,0,1,bidrequestid,,example.com;tpos=300&ptgt=a&slid=Midroll&slau=midroll&mind=30&maxd=60;`; const actualUrl = `${request.url}?${request.data}`; // Remove pvrn and vprn from both URLs before comparing const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); @@ -496,7 +544,7 @@ describe('fwsspBidAdapter', () => { const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest.gdprConsent, null, null); expect(userSyncs).to.deep.equal([{ type: 'image', - url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=consentString' + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&gdpr=1&gdpr_consent=consentString' }]); }); @@ -507,7 +555,7 @@ describe('fwsspBidAdapter', () => { const userSyncs = spec.getUserSyncs(syncOptions, null, bidderRequest.gdprConsent, bidderRequest.uspConsent, bidderRequest.gppConsent); expect(userSyncs).to.deep.equal([{ type: 'iframe', - url: 'https://ads.stickyadstv.com/auto-user-sync?gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid[]=8' + url: 'https://user-sync.fwmrm.net/ad/u?mode=auto-user-sync&gdpr=1&gdpr_consent=consentString&us_privacy=uspConsentString&gpp=gppString&gpp_sid=8' }]); }); }); @@ -538,7 +586,6 @@ describe('fwsspBidAdapter', () => { 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', 'flags': '+play', - 'videoAssetId': '0', 'mode': 'live', 'vclr': 'js-7.11.0-prebid-', 'timePosition': 120, @@ -1100,7 +1147,6 @@ describe('fwsspBidAdapter', () => { 'networkId': '42015', 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', - 'videoAssetId': '0', 'bidfloor': 1.25, 'bidfloorcur': 'GBP' } @@ -1127,7 +1173,6 @@ describe('fwsspBidAdapter', () => { 'networkId': '42015', 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', - 'videoAssetId': '0', 'bidfloor': 1.0, 'bidfloorcur': 'USD' }, @@ -1158,7 +1203,6 @@ describe('fwsspBidAdapter', () => { 'networkId': '42015', 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', - 'videoAssetId': '0' }, getFloor: () => ({ floor: 0, @@ -1187,7 +1231,6 @@ describe('fwsspBidAdapter', () => { 'networkId': '42015', 'profile': '42015:js_allinone_profile', 'siteSectionId': 'js_allinone_demo_site_section', - 'videoAssetId': '0' } }]; @@ -1197,4 +1240,109 @@ describe('fwsspBidAdapter', () => { expect(payload).to.include('_fw_bidfloorcur=USD'); }); }); + + describe('schain tests', () => { + const getBidRequests = () => { + return [{ + 'bidder': 'fwssp', + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'video': { + 'playerSize': [300, 600], + 'minduration': 30, + 'maxduration': 60, + } + }, + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'params': { + 'bidfloor': 2.00, + 'serverUrl': 'https://example.com/ad/g/1', + 'networkId': '42015', + 'profile': '42015:js_allinone_profile', + 'siteSectionId': 'js_allinone_demo_site_section', + 'flags': '+play', + 'videoAssetId': '0', + 'mode': 'live', + 'timePosition': 120, + 'tpos': 300, + 'slid': 'Midroll', + 'slau': 'midroll', + 'adRequestKeyValues': { + '_fw_player_width': '1920', + '_fw_player_height': '1080' + }, + 'gdpr_consented_providers': 'test_providers' + } + }] + }; + + const bidderRequest = { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true + }, + uspConsent: 'uspConsentString', + gppConsent: { + gppString: 'gppString', + applicableSections: [8] + }, + ortb2: { + regs: { + gpp: 'test_ortb2_gpp', + gpp_sid: 'test_ortb2_gpp_sid' + }, + site: { + content: { + id: 'test_content_id', + title: 'test_content_title' + } + } + }, + refererInfo: { + page: 'http://www.test.com' + } + }; + + it('should not include schain in the adrequest URL if schain is missing from bidrequest', () => { + const requests = spec.buildRequests(getBidRequests(), bidderRequest); + expect(requests).to.be.an('array').that.is.not.empty; + const request = requests[0]; + const expectedUrl = `https://example.com/ad/g/1?nw=42015&resp=vast4&prof=42015%3Ajs_allinone_profile&csid=js_allinone_demo_site_section&caid=0&flag=%2Bplay%2Bfwssp%2Bemcr%2Bnucr%2Baeti%2Brema%2Bexvt%2Bfwpbjs&mode=live&vclr=js-7.11.0-prebid-${pbjs.version};_fw_player_width=1920&_fw_player_height=1080&_fw_bidfloor=2&_fw_bidfloorcur=USD&_fw_gdpr_consent=consentString&_fw_gdpr=true&_fw_gdpr_consented_providers=test_providers&_fw_us_privacy=uspConsentString&gpp=gppString&gpp_sid=8&_fw_prebid_content=%7B%22id%22%3A%22test_content_id%22%2C%22title%22%3A%22test_content_title%22%7D&loc=http%3A%2F%2Fwww.test.com&_fw_video_context=&_fw_placement_type=null&_fw_plcmt_type=null;tpos=300&ptgt=a&slid=Midroll&slau=midroll&mind=30&maxd=60;`; + const actualUrl = `${request.url}?${request.data}`; + // Remove pvrn and vprn from both URLs before comparing + const cleanUrl = (url) => url.replace(/&pvrn=[^&]*/g, '').replace(/&vprn=[^&]*/g, ''); + expect(cleanUrl(actualUrl)).to.equal(cleanUrl(expectedUrl)); + }); + + it('should only encode comma within attribute value', () => { + const bidRequests = getBidRequests(); + const bidderRequest2 = { ...bidderRequest } + const schain1 = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'test1.com', + sid: '123,B', + hp: 1, + rid: 'bidrequestid1', + domain: 'test1.com' + }] + }; + + bidderRequest2.ortb2 = { + source: { + schain: schain1, + } + }; + const requests = spec.buildRequests(bidRequests, bidderRequest2); + const request = requests[0]; + + // schain check + const expectedEncodedSchainString = '1.0,1!test1.com,123%2CB,1,bidrequestid1,,test1.com'; + expect(request.data).to.include(expectedEncodedSchainString); + }); + }); }); diff --git a/test/spec/modules/greenbidsBidAdapter_specs.js b/test/spec/modules/greenbidsBidAdapter_spec.js similarity index 88% rename from test/spec/modules/greenbidsBidAdapter_specs.js rename to test/spec/modules/greenbidsBidAdapter_spec.js index 61e9c1d4e17..4cf992434dc 100644 --- a/test/spec/modules/greenbidsBidAdapter_specs.js +++ b/test/spec/modules/greenbidsBidAdapter_spec.js @@ -1,11 +1,46 @@ import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import { spec } from 'modules/greenbidsBidAdapter.js'; +import { spec, ENDPOINT_URL } from 'modules/greenbidsBidAdapter.js'; import { getScreenOrientation } from 'src/utils.js'; -const ENDPOINT = 'https://d.greenbids.ai/hb/bid-request'; const AD_SCRIPT = '"'; describe('greenbidsBidAdapter', () => { + const bidderRequestDefault = { + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000 + }; + + const bidRequests = [ + { + 'bidder': 'greenbids', + 'params': { + 'placementId': 4242 + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'creativeId': 'er2ee', + 'deviceWidth': 1680 + } + ]; + + function checkMediaTypesSizes(mediaTypes, expectedSizes) { + const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); + const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); + + return payloadWithBannerSizes.data.forEach(bid => { + if (Array.isArray(expectedSizes)) { + expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); + } else { + expect(bid.sizes[0]).to.equal(expectedSizes); + } + }); + } + const adapter = newBidder(spec); let sandbox; @@ -62,7 +97,7 @@ describe('greenbidsBidAdapter', () => { it('should send bid request to ENDPOINT via POST', function () { const request = spec.buildRequests(bidRequests, bidderRequestDefault); - expect(request.url).to.equal(ENDPOINT); + expect(request.url).to.equal(ENDPOINT_URL); expect(request.method).to.equal('POST'); }); @@ -319,74 +354,6 @@ describe('greenbidsBidAdapter', () => { expect(payload.device).to.deep.equal(ortb2DeviceBidderRequest.ortb2.device); }); - - it('should add hardwareConcurrency info to payload', function () { - const originalHardwareConcurrency = window.top.navigator.hardwareConcurrency; - - const mockHardwareConcurrency = (value) => { - Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { - value, - configurable: true, - }); - }; - - try { - const mockValue = 8; - mockHardwareConcurrency(mockValue); - - const requestWithHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); - const payloadWithHardwareConcurrency = JSON.parse(requestWithHardwareConcurrency.data); - - expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.exist; - expect(payloadWithHardwareConcurrency.hardwareConcurrency).to.deep.equal(mockValue); - - mockHardwareConcurrency(undefined); - - const requestWithoutHardwareConcurrency = spec.buildRequests(bidRequests, bidderRequestDefault); - const payloadWithoutHardwareConcurrency = JSON.parse(requestWithoutHardwareConcurrency.data); - - expect(payloadWithoutHardwareConcurrency.hardwareConcurrency).to.not.exist; - } finally { - Object.defineProperty(window.top.navigator, 'hardwareConcurrency', { - value: originalHardwareConcurrency, - configurable: true, - }); - } - }); - - it('should add deviceMemory info to payload', function () { - const originalDeviceMemory = window.top.navigator.deviceMemory; - - const mockDeviceMemory = (value) => { - Object.defineProperty(window.top.navigator, 'deviceMemory', { - value, - configurable: true, - }); - }; - - try { - const mockValue = 4; - mockDeviceMemory(mockValue); - - const requestWithDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); - const payloadWithDeviceMemory = JSON.parse(requestWithDeviceMemory.data); - - expect(payloadWithDeviceMemory.deviceMemory).to.exist; - expect(payloadWithDeviceMemory.deviceMemory).to.deep.equal(mockValue); - - mockDeviceMemory(undefined); - - const requestWithoutDeviceMemory = spec.buildRequests(bidRequests, bidderRequestDefault); - const payloadWithoutDeviceMemory = JSON.parse(requestWithoutDeviceMemory.data); - - expect(payloadWithoutDeviceMemory.deviceMemory).to.not.exist; - } finally { - Object.defineProperty(window.top.navigator, 'deviceMemory', { - value: originalDeviceMemory, - configurable: true, - }); - } - }); }); describe('pageTitle', function () { @@ -690,14 +657,20 @@ describe('greenbidsBidAdapter', () => { it('should add schain info to payload if available', function () { const bidRequest = Object.assign({}, bidRequests[0], { - schain: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'example.com', - sid: '00001', - hp: 1 - }] + ortb2: { + source: { + ext: { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + } + } + } } }); @@ -904,6 +877,7 @@ describe('greenbidsBidAdapter', () => { 'cpm': 0.5, 'currency': 'USD', 'height': 250, + 'size': '200x100', 'bidId': '3ede2a3fa0db94', 'ttl': 360, 'width': 300, @@ -914,6 +888,7 @@ describe('greenbidsBidAdapter', () => { 'cpm': 0.5, 'currency': 'USD', 'height': 200, + 'size': '300x150', 'bidId': '4fef3b4gb1ec15', 'ttl': 360, 'width': 350, @@ -939,6 +914,7 @@ describe('greenbidsBidAdapter', () => { 'cpm': 0.5, 'width': 300, 'height': 250, + 'size': '200x100', 'currency': 'USD', 'netRevenue': true, 'meta': { @@ -953,6 +929,7 @@ describe('greenbidsBidAdapter', () => { 'cpm': 0.5, 'width': 350, 'height': 200, + 'size': '300x150', 'currency': 'USD', 'netRevenue': true, 'meta': { @@ -993,39 +970,3 @@ describe('greenbidsBidAdapter', () => { }); }); }); - -const bidderRequestDefault = { - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000 -}; - -const bidRequests = [ - { - 'bidder': 'greenbids', - 'params': { - 'placementId': 4242 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'creativeId': 'er2ee', - 'deviceWidth': 1680 - } -]; - -function checkMediaTypesSizes(mediaTypes, expectedSizes) { - const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); - const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); - const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); - - return payloadWithBannerSizes.data.forEach(bid => { - if (Array.isArray(expectedSizes)) { - expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); - } else { - expect(bid.sizes[0]).to.equal(expectedSizes); - } - }); -} diff --git a/test/spec/modules/holidBidAdapter_spec.js b/test/spec/modules/holidBidAdapter_spec.js index 671427d3475..c3f7a873f5d 100644 --- a/test/spec/modules/holidBidAdapter_spec.js +++ b/test/spec/modules/holidBidAdapter_spec.js @@ -130,6 +130,50 @@ describe('holidBidAdapterTests', () => { ); expect(interpretedResponse[0].currency).to.equal(serverResponse.body.cur); }); + + it('should map adomain to meta.advertiserDomains and preserve existing meta fields', () => { + const serverResponseWithAdomain = { + body: { + id: 'test-id', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'testbidid-2', + impid: 'bid-id', + price: 0.55, + adm: '
ad
', + crid: 'cr-2', + w: 300, + h: 250, + // intentionally mixed-case + protocol + www to test normalization + adomain: ['https://Holid.se', 'www.Example.COM'], + ext: { + prebid: { + meta: { + networkId: 42 + } + } + } + } + ] + } + ] + } + }; + + const out = spec.interpretResponse(serverResponseWithAdomain, bidRequestData); + expect(out).to.have.length(1); + expect(out[0].requestId).to.equal('bid-id'); + + // critical assertion: advertiserDomains normalized and present + expect(out[0].meta).to.have.property('advertiserDomains'); + expect(out[0].meta.advertiserDomains).to.deep.equal(['holid.se', 'example.com']); + + // ensure any existing meta (e.g., networkId) is preserved + expect(out[0].meta.networkId).to.equal(42); + }); }); describe('getUserSyncs', () => { diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 1cd84e756d5..7249560c8c9 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -1381,10 +1381,6 @@ describe('ID5 ID System', function () { id5System.id5IdSubmodule._reset() }); - function verifyTagging(tagName, tagValue) { - verifyMultipleTagging({[tagName]: tagValue}) - } - function verifyMultipleTagging(tagsObj) { expect(window.googletag.cmd.length).to.be.at.least(1); window.googletag.cmd.forEach(cmd => cmd()); @@ -1410,69 +1406,27 @@ describe('ID5 ID System', function () { expect(window.googletag.cmd).to.have.lengthOf(0) }) - it('should set GAM targeting for id tag when universal_uid starts with ID5*', function () { - // Setup + it('should not set GAM targeting if not returned from the server', function () { let config = utils.deepClone(getId5FetchConfig()); config.params.gamTargetingPrefix = "id5"; - let testObj = {...storedObject, universal_uid: 'ID5*test123'}; - id5System.id5IdSubmodule.decode(testObj, config); - - verifyTagging('id', 'y'); - }) - - it('should set GAM targeting for ab tag with control value', function () { - // Setup - let testObj = {...storedObject, ab_testing: {result: 'control'}}; - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - verifyTagging('ab', 'c'); - }) - - it('should set GAM targeting for ab tag with normal value', function () { - // Setup - let testObj = {...storedObject, ab_testing: {result: 'normal'}}; - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - verifyTagging('ab', 'n'); - }) - - it('should set GAM targeting for enrich tag with enriched=true', function () { - // Setup - let testObj = {...storedObject, enrichment: {enriched: true}}; - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - verifyTagging('enrich', 'y'); - }) - - it('should set GAM targeting for enrich tag with enrichment_selected=true', function () { - // Setup - let testObj = {...storedObject, enrichment: {enrichment_selected: true}}; - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - verifyTagging('enrich', 's'); - }) - - it('should set GAM targeting for enrich tag with enrichment_selected=false', function () { - // Setup - let testObj = {...storedObject, enrichment: {enrichment_selected: false}}; - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - verifyTagging('enrich', 'c'); + id5System.id5IdSubmodule.decode(storedObject, getId5FetchConfig()); + expect(window.googletag.cmd).to.have.lengthOf(0) }) - it('should set GAM targeting for multiple tags when all conditions are met', function () { + it('should set GAM targeting when tags returned if fetch response', function () { // Setup + let config = utils.deepClone(getId5FetchConfig()); + config.params.gamTargetingPrefix = "id5"; let testObj = { ...storedObject, - universal_uid: 'ID5*test123', - ab_testing: {result: 'normal'}, - enrichment: {enriched: true} + "tags": { + "id": "y", + "ab": "n", + "enrich": "y" + } }; + id5System.id5IdSubmodule.decode(testObj, config); - // Call decode once with the combined test object - id5System.id5IdSubmodule.decode(testObj, targetingEnabledConfig); - - // Verify all tags were set correctly verifyMultipleTagging({ 'id': 'y', 'ab': 'n', diff --git a/test/spec/modules/intentIqAnalyticsAdapter_spec.js b/test/spec/modules/intentIqAnalyticsAdapter_spec.js index 76ecabf3460..664c6041ec8 100644 --- a/test/spec/modules/intentIqAnalyticsAdapter_spec.js +++ b/test/spec/modules/intentIqAnalyticsAdapter_spec.js @@ -7,10 +7,10 @@ import { EVENTS } from 'src/constants.js'; import * as events from 'src/events.js'; import { getStorageManager } from 'src/storageManager.js'; import sinon from 'sinon'; -import { REPORTER_ID, preparePayload } from '../../../modules/intentIqAnalyticsAdapter.js'; -import {FIRST_PARTY_KEY, PREBID, VERSION} from '../../../libraries/intentIqConstants/intentIqConstants.js'; +import { REPORTER_ID, preparePayload, restoreReportList } from '../../../modules/intentIqAnalyticsAdapter.js'; +import { FIRST_PARTY_KEY, PREBID, VERSION } from '../../../libraries/intentIqConstants/intentIqConstants.js'; import * as detectBrowserUtils from '../../../libraries/intentIqUtils/detectBrowserUtils.js'; -import {getReferrer, appendVrrefAndFui} from '../../../libraries/intentIqUtils/getRefferer.js'; +import { getReferrer, appendVrrefAndFui } from '../../../libraries/intentIqUtils/getRefferer.js'; import { gppDataHandler, uspDataHandler, gdprDataHandler } from '../../../src/consentHandler.js'; const partner = 10; @@ -22,6 +22,8 @@ const REPORT_SERVER_ADDRESS = 'https://test-reports.intentiq.com/report'; const storage = getStorageManager({ moduleType: 'analytics', moduleName: 'iiqAnalytics' }); +const randomVal = () => Math.floor(Math.random() * 100000) + 1 + const getUserConfig = () => [ { 'name': 'intentIqId', @@ -45,8 +47,6 @@ const getUserConfigWithReportingServerAddress = () => [ 'params': { 'partner': partner, 'unpack': null, - 'manualWinReportEnabled': false, - 'reportingServerAddress': REPORT_SERVER_ADDRESS }, 'storage': { 'type': 'html5', @@ -57,7 +57,7 @@ const getUserConfigWithReportingServerAddress = () => [ } ]; -const wonRequest = { +const getWonRequest = () => ({ 'bidderCode': 'pubmatic', 'width': 728, 'height': 90, @@ -65,7 +65,7 @@ const wonRequest = { 'adId': '23caeb34c55da51', 'requestId': '87615b45ca4973', 'transactionId': '5e69fd76-8c86-496a-85ce-41ae55787a50', - 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf', + 'auctionId': '0cbd3a43-ff45-47b8-b002-16d3946b23bf-' + randomVal(), 'mediaType': 'banner', 'source': 'client', 'cpm': 5, @@ -87,7 +87,15 @@ const wonRequest = { 'pbCg': '', 'size': '728x90', 'status': 'rendered' -}; +}); + +const enableAnalyticWithSpecialOptions = (options) => { + iiqAnalyticsAnalyticsAdapter.disableAnalytics() + iiqAnalyticsAnalyticsAdapter.enableAnalytics({ + provider: 'iiqAnalytics', + options + }) +} describe('IntentIQ tests all', function () { let logErrorStub; @@ -138,8 +146,11 @@ describe('IntentIQ tests all', function () { }); it('should send POST request with payload in request body if reportMethod is POST', function () { + enableAnalyticWithSpecialOptions({ + reportMethod: 'POST' + }) const [userConfig] = getUserConfig(); - userConfig.params.reportMethod = 'POST'; + const wonRequest = getWonRequest(); config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -149,6 +160,7 @@ describe('IntentIQ tests all', function () { events.emit(EVENTS.BID_WON, wonRequest); const request = server.requests[0]; + restoreReportList(); const expectedData = preparePayload(wonRequest); const expectedPayload = `["${btoa(JSON.stringify(expectedData))}"]`; @@ -159,6 +171,7 @@ describe('IntentIQ tests all', function () { it('should send GET request with payload in query string if reportMethod is NOT provided', function () { const [userConfig] = getUserConfig(); + const wonRequest = getWonRequest(); config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -173,6 +186,7 @@ describe('IntentIQ tests all', function () { const payloadEncoded = url.searchParams.get('payload'); const decoded = JSON.parse(atob(JSON.parse(payloadEncoded)[0])); + restoreReportList(); const expected = preparePayload(wonRequest); expect(decoded.partnerId).to.equal(expected.partnerId); @@ -182,9 +196,9 @@ describe('IntentIQ tests all', function () { it('IIQ Analytical Adapter bid win report', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); - getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({href: 'http://localhost:9876'}); + getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876' }); const expectedVrref = getWindowLocationStub().href; - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -200,7 +214,7 @@ describe('IntentIQ tests all', function () { it('should include adType in payload when present in BID_WON event', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - const bidWonEvent = { ...wonRequest, mediaType: 'video' }; + const bidWonEvent = { ...getWonRequest(), mediaType: 'video' }; events.emit(EVENTS.BID_WON, bidWonEvent); @@ -214,10 +228,10 @@ describe('IntentIQ tests all', function () { }); it('should include adType in payload when present in reportExternalWin event', function () { + enableAnalyticWithSpecialOptions({ manualWinReportEnabled: true }) getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); const externalWinEvent = { cpm: 1, currency: 'USD', adType: 'banner' }; const [userConfig] = getUserConfig(); - userConfig.params.manualWinReportEnabled = true; config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); @@ -241,7 +255,7 @@ describe('IntentIQ tests all', function () { const uspStub = sinon.stub(uspDataHandler, 'getConsentData').returns('1NYN'); const gdprStub = sinon.stub(gdprDataHandler, 'getConsentData').returns({ consentString: 'gdprConsent' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -260,7 +274,7 @@ describe('IntentIQ tests all', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); const expectedVrref = encodeURIComponent('http://localhost:9876/'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -273,11 +287,13 @@ describe('IntentIQ tests all', function () { it('should handle BID_WON event with default group configuration', function () { localStorage.setItem(FIRST_PARTY_KEY, defaultData); const defaultDataObj = JSON.parse(defaultData) + const wonRequest = getWonRequest(); events.emit(EVENTS.BID_WON, wonRequest); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; + restoreReportList() const dataToSend = preparePayload(wonRequest); const base64String = btoa(JSON.stringify(dataToSend)); const payload = encodeURIComponent(JSON.stringify([base64String])); @@ -301,7 +317,7 @@ describe('IntentIQ tests all', function () { getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -317,14 +333,14 @@ describe('IntentIQ tests all', function () { it('should not send request if manualWinReportEnabled is true', function () { iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.equal(1); }); it('should read data from local storage', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"group": "A"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid", "eidl": 10}'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(iiqAnalyticsAnalyticsAdapter.initOptions.dataInLs).to.equal('testpcid'); expect(iiqAnalyticsAnalyticsAdapter.initOptions.eidl).to.equal(10); expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('A'); @@ -333,18 +349,18 @@ describe('IntentIQ tests all', function () { it('should handle initialization values from local storage', function () { localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(iiqAnalyticsAnalyticsAdapter.initOptions.currentGroup).to.equal('B'); expect(iiqAnalyticsAnalyticsAdapter.initOptions.fpid).to.be.not.null; }); it('should handle reportExternalWin', function () { events.emit(EVENTS.BID_REQUESTED); - iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = true; + iiqAnalyticsAnalyticsAdapter.initOptions.manualWinReportEnabled = false; localStorage.setItem(FIRST_PARTY_KEY, '{"pcid":"testpcid", "group": "B"}'); localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, '{"data":"testpcid"}'); expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin).to.be.a('function'); - expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({cpm: 1, currency: 'USD'})).to.equal(false); + expect(window[`intentIqAnalyticsAdapter_${partner}`].reportExternalWin({ cpm: 1, currency: 'USD' })).to.equal(false); }); it('should return window.location.href when window.self === window.top', function () { @@ -387,7 +403,7 @@ describe('IntentIQ tests all', function () { detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('chrome'); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.equal(0); }); @@ -401,7 +417,7 @@ describe('IntentIQ tests all', function () { detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -419,9 +435,10 @@ describe('IntentIQ tests all', function () { config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG_BROWSER); detectBrowserStub = sinon.stub(detectBrowserUtils, 'detectBrowser').returns('safari'); + enableAnalyticWithSpecialOptions({ reportingServerAddress: REPORT_SERVER_ADDRESS }) localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -431,7 +448,7 @@ describe('IntentIQ tests all', function () { it('should include source parameter in report URL', function () { localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify(defaultData)); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(server.requests.length).to.be.above(0); @@ -447,7 +464,7 @@ describe('IntentIQ tests all', function () { sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(USERID_CONFIG); localStorage.setItem(FIRST_PARTY_KEY, `${FIRST_PARTY_KEY}${siloEnabled ? '_p_' + partner : ''}`); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); expect(server.requests.length).to.be.above(0); const request = server.requests[0]; @@ -466,7 +483,7 @@ describe('IntentIQ tests all', function () { sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(request.url).to.include('general=Lee'); @@ -485,7 +502,7 @@ describe('IntentIQ tests all', function () { sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns(userConfig); localStorage.setItem(FIRST_PARTY_KEY, defaultData); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; expect(request.url).not.to.include('general'); @@ -494,10 +511,10 @@ describe('IntentIQ tests all', function () { const spdObject = { foo: 'bar', value: 42 }; const expectedSpdEncoded = encodeURIComponent(JSON.stringify(spdObject)); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: spdObject})); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ ...defaultData, spd: spdObject })); getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; @@ -509,10 +526,10 @@ describe('IntentIQ tests all', function () { const spdObject = 'server provided data'; const expectedSpdEncoded = encodeURIComponent(spdObject); - localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({...defaultData, spd: spdObject})); + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ ...defaultData, spd: spdObject })); getWindowLocationStub = sinon.stub(utils, 'getWindowLocation').returns({ href: 'http://localhost:9876/' }); - events.emit(EVENTS.BID_WON, wonRequest); + events.emit(EVENTS.BID_WON, getWonRequest()); const request = server.requests[0]; @@ -520,6 +537,99 @@ describe('IntentIQ tests all', function () { expect(request.url).to.include(`&spd=${expectedSpdEncoded}`); }); + describe('GAM prediction reporting', function () { + function createMockGAM() { + const listeners = {}; + return { + cmd: [], + pubads: () => ({ + addEventListener: (name, cb) => { + listeners[name] = cb; + } + }), + _listeners: listeners + }; + } + + function withConfigGamPredict(gamObj) { + const [userConfig] = getUserConfig(); + userConfig.params.gamObjectReference = gamObj; + userConfig.params.gamPredictReporting = true; + config.getConfig.restore(); + sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); + } + + it('should subscribe to GAM and send report on slotRenderEnded without prior bidWon', function () { + const gam = createMockGAM(); + withConfigGamPredict(gam); + + // enable subscription by LS flag + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + + // provide recent auctionEnd with matching bid to enrich payload + events.getEvents.restore(); + sinon.stub(events, 'getEvents').returns([ + { + eventType: 'auctionEnd', args: { + auctionId: 'auc-1', + adUnitCodes: ['ad-unit-1'], + bidsReceived: [{ bidder: 'pubmatic', adUnitCode: 'ad-unit-1', cpm: 1, currency: 'USD', originalCpm: 1, originalCurrency: 'USD', status: 'rendered' }] + } + } + ]); + + // trigger adapter to subscribe + events.emit(EVENTS.BID_REQUESTED); + + // execute GAM cmd to register listener + gam.cmd.forEach(fn => fn()); + + // simulate slotRenderEnded + const slot = { + getSlotElementId: () => 'ad-unit-1', + getAdUnitPath: () => '/123/foo', + getTargetingKeys: () => ['hb_bidder', 'hb_adid'], + getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + }; + if (gam._listeners['slotRenderEnded']) { + gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + } + + expect(server.requests.length).to.be.above(0); + }); + + it('should NOT send report if a matching bidWon already exists', function () { + const gam = createMockGAM(); + withConfigGamPredict(gam); + + localStorage.setItem(FIRST_PARTY_KEY + '_' + partner, JSON.stringify({ gpr: true })); + localStorage.setItem(FIRST_PARTY_KEY, defaultData); + + // provide prior bidWon matching placementId and hb_adid + events.getEvents.restore(); + sinon.stub(events, 'getEvents').returns([ + { eventType: 'bidWon', args: { adId: 'ad123' }, id: 'ad-unit-1' } + ]); + + events.emit(EVENTS.BID_REQUESTED); + gam.cmd.forEach(fn => fn()); + + const slot = { + getSlotElementId: () => 'ad-unit-1', + getAdUnitPath: () => '/123/foo', + getTargetingKeys: () => ['hb_bidder', 'hb_adid'], + getTargeting: (k) => k === 'hb_bidder' ? ['pubmatic'] : k === 'hb_adid' ? ['ad123'] : [] + }; + + const initialRequests = server.requests.length; + if (gam._listeners['slotRenderEnded']) { + gam._listeners['slotRenderEnded']({ isEmpty: false, slot }); + } + expect(server.requests.length).to.equal(initialRequests); + }); + }); + const testCasesVrref = [ { description: 'domainName matches window.top.location.href', @@ -636,12 +746,12 @@ describe('IntentIQ tests all', function () { adUnitConfigTests.forEach(({ adUnitConfig, description, event, expectedPlacementId }) => { it(description, function () { const [userConfig] = getUserConfig(); - userConfig.params.adUnitConfig = adUnitConfig; + enableAnalyticWithSpecialOptions({ adUnitConfig }) config.getConfig.restore(); sinon.stub(config, 'getConfig').withArgs('userSync.userIds').returns([userConfig]); - const testEvent = { ...wonRequest, ...event }; + const testEvent = { ...getWonRequest(), ...event }; events.emit(EVENTS.BID_WON, testEvent); const request = server.requests[0]; diff --git a/test/spec/modules/intentIqIdSystem_spec.js b/test/spec/modules/intentIqIdSystem_spec.js index 00ca52bf80a..42cff5c2582 100644 --- a/test/spec/modules/intentIqIdSystem_spec.js +++ b/test/spec/modules/intentIqIdSystem_spec.js @@ -393,6 +393,46 @@ describe('IntentIQ tests', function () { expect(targetingKeys).to.include(customParamName); }); + it('should NOT call GAM setTargeting when current browser is in browserBlackList', function () { + const usedBrowser = 'chrome'; + const gam = mockGAM(); + const pa = gam.pubads(); + sinon.stub(gam, 'pubads').returns(pa); + + const originalSetTargeting = pa.setTargeting; + let setTargetingCalls = 0; + pa.setTargeting = function (...args) { + setTargetingCalls++; + return originalSetTargeting.apply(this, args); + }; + + localStorage.setItem(FIRST_PARTY_KEY, JSON.stringify({ + pcid: 'pcid-1', + pcidDate: Date.now(), + group: 'A', + isOptedOut: false, + date: Date.now(), + sCal: Date.now() + })); + + const cfg = { + params: { + partner, + gamObjectReference: gam, + gamParameterName: 'custom_gam_param', + browserBlackList: usedBrowser + } + }; + + intentIqIdSubmodule.getId(cfg); + gam.cmd.forEach(fn => fn()); + const currentBrowserLowerCase = detectBrowser(); + if (currentBrowserLowerCase === usedBrowser) { + expect(setTargetingCalls).to.equal(0); + expect(pa.getTargetingKeys()).to.not.include('custom_gam_param'); + } + }); + it('should not throw Uncaught TypeError when IntentIQ endpoint returns empty response', async function () { const callBackSpy = sinon.spy(); const submoduleCallback = intentIqIdSubmodule.getId(defaultConfigParams).callback; diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js index 187ccf9459f..4e70ac6f9f3 100644 --- a/test/spec/modules/koblerBidAdapter_spec.js +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {pageViewId, spec} from 'modules/koblerBidAdapter.js'; +import {spec} from 'modules/koblerBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; @@ -7,7 +7,7 @@ import {getRefererInfo} from 'src/refererDetection.js'; import { setConfig as setCurrencyConfig } from '../../../modules/currency.js'; import { addFPDToBidderRequest } from '../../helpers/fpd.js'; -function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}) { +function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}, pageViewId) { const gdprConsent = { consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', apiVersion: 2, @@ -21,7 +21,8 @@ function createBidderRequest(auctionId, timeout, pageUrl, gdprVendorData = {}) { refererInfo: { page: pageUrl || 'example.com' }, - gdprConsent: gdprConsent + gdprConsent: gdprConsent, + pageViewId }; } @@ -259,15 +260,17 @@ describe('KoblerAdapter', function () { const testUrl = 'kobler.no'; const auctionId1 = '8319af54-9795-4642-ba3a-6f57d6ff9100'; const auctionId2 = 'e19f2d0c-602d-4969-96a1-69a22d483f47'; + const pageViewId1 = '2949ce3c-2c4d-4b96-9ce0-8bf5aa0bb416'; + const pageViewId2 = '6c449b7d-c9b0-461d-8cc7-ce0a8da58349'; const timeout = 5000; const validBidRequests = [createValidBidRequest()]; - const bidderRequest1 = createBidderRequest(auctionId1, timeout, testUrl); - const bidderRequest2 = createBidderRequest(auctionId2, timeout, testUrl); + const bidderRequest1 = createBidderRequest(auctionId1, timeout, testUrl, {}, pageViewId1); + const bidderRequest2 = createBidderRequest(auctionId2, timeout, testUrl, {}, pageViewId2); const openRtbRequest1 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest1).data); - expect(openRtbRequest1.ext.kobler.page_view_id).to.be.equal(pageViewId); + expect(openRtbRequest1.ext.kobler.page_view_id).to.be.equal(pageViewId1); const openRtbRequest2 = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest2).data); - expect(openRtbRequest2.ext.kobler.page_view_id).to.be.equal(pageViewId); + expect(openRtbRequest2.ext.kobler.page_view_id).to.be.equal(pageViewId2); }); it('should read data from valid bid requests', function () { @@ -440,6 +443,7 @@ describe('KoblerAdapter', function () { }); it('should create whole OpenRTB request', function () { + const pageViewId = 'aa9f0b20-a642-4d0e-acb5-e35805253ef7'; const validBidRequests = [ createValidBidRequest( { @@ -483,7 +487,8 @@ describe('KoblerAdapter', function () { } } } - } + }, + pageViewId ); const result = spec.buildRequests(validBidRequests, bidderRequest); diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index 61248ffcf3e..e3933a966ac 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -735,6 +735,85 @@ describe('Media.net Analytics Adapter', function () { }).catch(done); }); + it('should set serverLatencyMillis and filtered pbsExt for S2S bids on AUCTION_END', function (done) { + // enable analytics and start an S2S auction flow + medianetAnalytics.clearlogsQueue(); + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_S2S_BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.MNET_S2S_BID_RESPONSE); + + // craft bidderRequests with S2S info and pbs ext including debug + const bidderRequestsWithExt = Object.assign({}, MOCK.MNET_S2S_BID_REQUESTED, { + serverResponseTimeMs: 123, + pbsExt: { foo: 'bar', baz: 1, debug: { trace: true } } + }); + + // trigger AUCTION_END with the enriched bidderRequests + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, { bidderRequests: [bidderRequestsWithExt] })); + // advance fake timers to allow async auctionEnd processing to run + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + // inspect internal auctions state through prebid global + const auctions = getGlobal().medianetGlobals.analytics.auctions; + const auctionObj = auctions[MOCK.AUCTION_END.auctionId]; + expect(auctionObj).to.exist; + + // locate the bid by id from the S2S request + const bidId = MOCK.MNET_S2S_BID_REQUESTED.bids[0].bidId; + const bidObj = auctionObj.bidsReceived.find(b => b.bidId === bidId); + expect(bidObj).to.exist; + expect(bidObj.serverLatencyMillis).to.equal(123); + // pbsExt should not include 'debug' + expect(bidObj.pbsExt).to.deep.equal({ foo: 'bar', baz: 1 }); + done(); + }).catch(done); + }); + + it('should map PBS server error to bid status for S2S timed-out bids', function (done) { + // Start S2S auction and create a timed-out bid for the same bidId + medianetAnalytics.clearlogsQueue(); + const auctionId = MOCK.MNET_S2S_BID_REQUESTED.auctionId; + const bidId = MOCK.MNET_S2S_BID_REQUESTED.bids[0].bidId; + + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.AD_UNITS})); + events.emit(BID_REQUESTED, MOCK.MNET_S2S_BID_REQUESTED); + + // mark the bid as timed out so bidsReceived contains a non-success entry + const timedOut = [{ + bidId, + bidder: 'medianet', + adUnitCode: MOCK.MNET_S2S_BID_REQUESTED.bids[0].adUnitCode, + auctionId, + params: MOCK.MNET_S2S_BID_REQUESTED.bids[0].params, + timeout: 100, + src: 's2s' + }]; + events.emit(BID_TIMEOUT, timedOut); + + // bidderRequests with serverErrors (e.g., 501) + const bidderRequestsWithError = Object.assign({}, MOCK.MNET_S2S_BID_REQUESTED, { + serverResponseTimeMs: 50, + pbsExt: {}, + serverErrors: [{ code: 501 }] + }); + + events.emit(AUCTION_END, Object.assign({}, MOCK.AUCTION_END, { bidderRequests: [bidderRequestsWithError] })); + // advance fake timers to allow async auctionEnd processing to run + clock.tick(2000); + + waitForPromiseResolve(Promise.resolve()).then(() => { + const auctions = getGlobal().medianetGlobals.analytics.auctions; + const auctionObj = auctions[auctionId]; + expect(auctionObj).to.exist; + const bidObj = auctionObj.bidsReceived.find(b => b.bidId === bidId); + expect(bidObj).to.exist; + // 2000 (PBS_ERROR_STATUS_START) + 501 + expect(bidObj.status).to.equal(2000 + 501); + done(); + }).catch(done); + }); + it('should handle currency conversion from JPY to USD', function (done) { const prebidGlobal = getGlobal(); prebidGlobal.convertCurrency = prebidGlobal.convertCurrency || function () { @@ -747,7 +826,7 @@ describe('Media.net Analytics Adapter', function () { waitForPromiseResolve(Promise.resolve()).then(() => { const queue = medianetAnalytics.getlogsQueue(); - expect(queue.length).equals(1); + expect(queue.length).to.be.greaterThan(0); const currencyLog = queue.map((log) => getQueryData(log, true))[0]; expect(currencyLog.curr).to.have.ordered.members(['', 'JPY', '']); diff --git a/test/spec/modules/msftBidAdapter_spec.js b/test/spec/modules/msftBidAdapter_spec.js new file mode 100644 index 00000000000..9f7d757d897 --- /dev/null +++ b/test/spec/modules/msftBidAdapter_spec.js @@ -0,0 +1,1441 @@ +import { + expect +} from 'chai'; +import { + spec +} from 'modules/msftBidAdapter.js'; +import { + BANNER, + VIDEO, + NATIVE +} from '../../../src/mediaTypes.js'; +import { + deepClone +} from '../../../src/utils.js'; + +const ENDPOINT_URL_NORMAL = 'https://ib.adnxs.com/openrtb2/prebidjs'; + +describe('msftBidAdapter', function () { + const baseBidRequests = { + bidder: 'msft', + adUnitCode: 'adunit-code', + bidId: '2c5f3044f546f1', + params: { + placement_id: 12345 + } + }; + + const baseBidderRequest = { + auctionId: 'test-auction-id', + ortb2: { + site: { + page: 'http://www.example.com/page.html', + domain: 'example.com' + }, + user: { + ext: { + eids: [{ + source: 'adserver.org', + uids: [{ + id: '12345', + atype: 1 + }] + }, { + source: 'uidapi.com', + uids: [{ + id: '12345', + atype: 1 + }] + }] + } + } + }, + refererInfo: { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": ['http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true'], + "topmostLocation": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "location": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "canonicalUrl": 'http://www.example.com/page.html', + "page": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "domain": "test.localhost:9999", + "ref": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "canonicalUrl": null + } + }, + bids: baseBidRequests, + gdprConsent: { + gdprApplies: true, + consentString: 'test-consent-string', + vendorData: { + purpose: { + consents: { + 1: true + } + } + } + } + }; + + describe('isBidRequestValid', function () { + it('should return true when required params are present (placement_id)', function () { + const bid = { + bidder: 'msft', + params: { + placement_id: 12345 + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return true when required params are present (member and inv_code)', function () { + const bid = { + bidder: 'msft', + params: { + member: 123, + inv_code: 'abc' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not present', function () { + const bid = { + bidder: 'msft', + params: { + member: 123 + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when required params are not the correct type', function () { + const bid = { + bidder: 'msft', + params: { + placement_id: '12345' + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(false, 'placement_id is string, should be number'); + + bid.params = { + member: '123', + inv_code: 'abc' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false, 'member is string, should be number'); + + bid.params = { + member: 123, + inv_code: 123 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false, 'inv_code is number, should be string'); + }); + + it('should build a basic banner request', function () { + let testBidRequest = deepClone(baseBidRequests); + testBidRequest.params = Object.assign({}, testBidRequest.params, { + banner_frameworks: [1, 2, 6], + allow_smaller_sizes: false, + use_pmt_rule: true, + keywords: 'sports,music=rock', + traffic_source_code: 'some_traffic_source', + pubclick: 'http://publisher.click.url', + ext_inv_code: 'inv_code_123', + ext_imp_id: 'ext_imp_id_456' + }); + const bidRequests = [{ + ...testBidRequest, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + }]; + + const testBidderRequest = deepClone(baseBidderRequest); + const bidderRequest = Object.assign({}, testBidderRequest, { + bids: bidRequests + }); + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.satisfy(url => url.startsWith(ENDPOINT_URL_NORMAL)); + const data = request.data; + expect(data).to.exist; + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].banner.format).to.deep.equal([{ + w: 300, + h: 250 + }, { + w: 300, + h: 600 + }]); + expect(data.imp[0].banner.api).to.deep.equal([1, 2, 6]); + expect(data.imp[0].ext.appnexus.placement_id).to.equal(12345); + expect(data.imp[0].ext.appnexus.allow_smaller_sizes).to.equal(false); + expect(data.imp[0].ext.appnexus.use_pmt_rule).to.equal(true); + expect(data.imp[0].ext.appnexus.keywords).to.equal('sports,music=rock'); + expect(data.imp[0].ext.appnexus.traffic_source_code).to.equal('some_traffic_source'); + expect(data.imp[0].ext.appnexus.pubclick).to.equal('http://publisher.click.url'); + expect(data.imp[0].ext.appnexus.ext_inv_code).to.equal('inv_code_123'); + expect(data.imp[0].id).to.equal('ext_imp_id_456'); + }); + + it('should build a banner request without eids but request.user.ext exists', function () { + let testBidRequest = deepClone(baseBidRequests); + // testBidRequest.user.ext = {}; + const bidRequests = [{ + ...testBidRequest, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + }]; + const testBidderRequest = deepClone(baseBidderRequest); + delete testBidderRequest.ortb2.user.ext.eids; // no eids + const bidderRequest = Object.assign({}, testBidderRequest, { + bids: bidRequests + }); + debugger;// eslint-disable-line no-debugger + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + const data = request.data; + expect(data).to.exist; + expect(data.user.ext).to.exist; + }); + + if (FEATURES.VIDEO) { + it('should build a video request', function () { + const testBidRequests = deepClone(baseBidRequests); + const testBidderRequest = deepClone(baseBidderRequest); + + const bidRequests = [{ + ...testBidRequests, + mediaTypes: { + video: { + context: 'instream', + playerSize: [ + [640, 480] + ], + plcmt: 4, + mimes: ['video/mp4'], + protocols: [2, 3], + api: [2] + } + } + }]; + const bidderRequest = Object.assign({}, testBidderRequest, { + bids: bidRequests + }); + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.satisfy(url => url.startsWith(ENDPOINT_URL_NORMAL)); + const data = request.data; + expect(data).to.exist; + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].video).to.exist; + expect(data.imp[0].video.placement).to.equal(4); + expect(data.imp[0].video.w).to.equal(640); + expect(data.imp[0].video.h).to.equal(480); + expect(data.imp[0].ext.appnexus.require_asset_url).to.be.true; + }); + } + + if (FEATURES.NATIVE) { + it('should build a native request', function () { + const testBidRequest = deepClone(baseBidRequests); + const testBidderRequest = deepClone(baseBidderRequest); + + testBidRequest.params = { + member: 123, + inv_code: 'inv_code_123' + } + const nativeRequest = { + assets: [{ + id: 1, + required: 1, + title: { + len: 140 + } + }], + context: 1, + plcmttype: 1, + ver: '1.2' + }; + + const bidRequests = [{ + ...testBidRequest, + mediaTypes: { + native: { + ortb: nativeRequest + } + }, + nativeOrtbRequest: nativeRequest, + nativeParams: { + ortb: nativeRequest + } + }]; + const bidderRequest = Object.assign({}, testBidderRequest, { + bids: bidRequests + }); + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.satisfy(url => url.startsWith(ENDPOINT_URL_NORMAL)); + expect(request.url).to.satisfy(url => url.indexOf('member_id=123') !== -1); + const data = request.data; + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].native.request).to.equal(JSON.stringify(nativeRequest)); + }); + } + }); + + describe('interpretResponse', function () { + const bannerBidderRequest = { + "bidderCode": "msft", + "auctionId": null, + "bidderRequestId": "f8c98171-d21f-4087-a1be-f72be8136dcc", + "bids": [{ + "bidder": "msft", + "params": { + "placement_id": 13144370 + }, + "ortb2Imp": { + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "/19968336/header-bid-tag-0" + } + }, + "gpid": "/19968336/header-bid-tag-0" + } + }, + "mediaTypes": { + "banner": { + "sizes": [ + [ + 300, + 250 + ] + ] + } + }, + "adUnitCode": "div-gpt-ad-1460505748561-0", + "transactionId": null, + "adUnitId": "94211d51-e391-4939-b965-bd8e974dca92", + "sizes": [ + [ + 300, + 250 + ] + ], + "bidId": "453e250c-a12c-420b-8539-ee0ef2f4868e", + "bidderRequestId": "f8c98171-d21f-4087-a1be-f72be8136dcc", + "auctionId": null, + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "ref": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + } + }], + "auctionStart": 1759244033417, + "timeout": 1000, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "topmostLocation": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "location": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "canonicalUrl": null, + "page": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "domain": "test.localhost:9999", + "ref": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "canonicalUrl": null + } + }, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true", + "ref": "http://test.localhost:9999/integrationExamples/gpt/hello_world_2.html?pbjs_debug=true" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + }, + "start": 1759244033424 + }; + + const bannerBidResponse = { + "body": { + "id": "099630d6-1943-43ef-841d-fe916871e00a", + "seatbid": [{ + "bid": [{ + "id": "2609670786764493419", + "impid": "453e250c-a12c-420b-8539-ee0ef2f4868e", + "price": 1.5, + "adid": "96846035", + "adm": "", + "adomain": [ + "prebid.org" + ], + "iurl": "https://nym2-ib.adnxs.com/cr?id=96846035", + "cid": "9325", + "crid": "96846035", + "h": 250, + "w": 300, + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 5070987573008590000, + "bidder_id": 2, + "bid_ad_type": 0, + "buyer_line_item_id": 0, + "seller_line_item_id": 0, + "curator_line_item_id": 0, + "advertiser_id": 2529885, + "renderer_id": 0, + "no_ad_url": "https://nym2-ib.adnxs.com/it?an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2Fgpt%2Fhello_world_2.html%3Fpbjs_debug%3Dtrue&e=wqT_3QKYDKAYBgAAAwDWAAUBCMTe78YGEObJ46zJivGvRhjGpKbEk569pU4qNgkAAAkCABEJBywAABkAAADA9Sj4PyEREgApEQkAMREb9A4BMLKiogY47UhA7UhIAFAAWJzxW2AAaLXPeXgAgAEBigEAkgEDVVNEmAGsAqAB-gGoAQGwAQC4AQLAAQDIAQLQAQDYAQDgAQDwAQCKAil1ZignYScsIDI1Mjk4ODUsIDApO3VmKCdyJywgOTY4NDYwMzUsIDApO5ICpQQhVm1BdHdnakt2Sm9kRU5PQmx5NFlBQ0NjOFZzd0FEZ0FRQVJJN1VoUXNxS2lCbGdBWU00QmFBQndGSGdJZ0FFVWlBRUlrQUVCbUFFQm9BRUJxQUVEc0FFQXVRRy0wRXpGQUFENFA4RUJ2dEJNeFFBQS1EX0pBZmg3cVp5SWNQRV8yUUVBQUFBQUFBRHdQLUFCQVBVQgURKEpnQ0FLQUNBTFVDBRAETDAJCPBJTUFDQU1nQ0FOQUNBTmdDQU9BQ0FPZ0NBUGdDQUlBREFaZ0RBYm9EQ1U1WlRUSTZOakl5TnVBRHFrcUlCQUNRQkFDWUJBSEJCQUEFVwEBCHlRUQEHCQEYTmdFQVBFRQkNAQEgQ0lCZEl3cVFVAQ0gQUFBRHdQN0VGAQoJAQhEQkIdPwB5FSgMQUFBTjIoAABaLigAuDRBWHdrd253QmUzODJnTDRCZDIwbWdHQ0JnTlZVMFNJQmdDUUJnR1lCZ0NoQmdBAU40QUFQZ19xQVlCc2dZa0MddABFHQwARx0MAEkdDKB1QVlLLUFlNDB3ajRCNkxWQ1BnSDdkd0ktQWVvN0FqNEJfUDhDSUVJQQlqYEFBQUNJQ0FDUUNBQS6aApUBIUtSQXh5UWoyKQIkblBGYklBUW9BRDEwWEQ0UHpvSlRsbE5Nam8yTWpJMlFLcEtTEYkMUEFfVREMDEFBQVcdDABZHQwAYR0MAGMdDBBlQUNKQR0QeMICNWh0dHBzOi8vZG9jcy5wcmViaWQub3JnL2Rldi0BFPBhL2dldHRpbmctc3RhcnRlZC5odG1s2AL36QPgAq2YSOoCVWh0dHA6Ly90ZXN0LmxvY2FsaG9zdDo5OTk5L2ludGVncmF0aW9uRXhhbXBsZXMvZ3B0L2hlbGxvX3dvcmxkXzIFUvBGP3BianNfZGVidWc9dHJ1ZYADAIgDAZADAJgDFKADAaoDAkgAwAPYBMgDANgDAOADAOgDAPgDA4AEAJIEEi9vcGVucnRiMi8JwfBhanOYBACiBA4xMDAuMTQuMTYzLjI1MKgE_UOyBA4IABAAGAAgADAAOAJCALgEAMAEgNq4IsgEANIEDjkzMjUjTllNMjo2MjI22gQCCADgBADwBNOBly6IBQGYBQCgBf____8FA7ABqgUkMDk5NjMwZDYtMTk0My00M2VmLTg0MWQtZmU5MTY4NzFlMDBhwAUAyQWJtBTwP9IFCQkJDDQAANgFAeAFAfAFAfoFBAGXKJAGAJgGALgGAMEGCSMo8L_QBvUv2gYWChAJERkBAcVg4AYB8gYCCACABwGIBwCgBwHIB4j9BdIHDxViASYQIADaBwYBX_CBGADgBwDqBwIIAPAHkPWmA4oIYQpdAAABmZseoaBGX8RUlZjk5qh-YEbiixFeNKSwU942xVq95IWMpLMfZlV-kwZx7igi_tadimiKAcrhNH810Dec1tTfiroSFHftKanxAhowy564iuN_tWpE5xar7QwcEAGVCAAAgD-YCAHACADSCA2HMNoIBAgAIADgCADoCAA.&s=6644c05b4a0f8a14c7aae16b72c1408265651a7e" + } + } + }], + "seat": "9325" + }], + "bidid": "5488067055951399787", + "cur": "USD", + "ext": { + "tmaxrequest": 150 + } + }, + "headers": {} + }; + + const videoInstreamBidderRequest = { + "bidderCode": "msft", + "auctionId": null, + "bidderRequestId": "ba7c3a68-9f32-4fab-97dc-d016fcef290b", + "bids": [{ + "bidder": "msft", + "params": { + "placement_id": 31523633 + }, + "ortb2Imp": { + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm", + "application/vnd.apple.mpegurl", + "application/javascript" + ], + "startdelay": 0, + "protocols": [ + 2, + 3, + 7 + ], + "w": 640, + "h": 360, + "placement": 1, + "maxextended": -1, + "boxingallowed": 1, + "playbackmethod": [ + 3 + ], + "playbackend": 1, + "pos": 1, + "api": [ + 2 + ] + }, + "ext": { + "data": {} + } + }, + "mediaTypes": { + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm", + "application/vnd.apple.mpegurl", + "application/javascript" + ], + "protocols": [ + 2, + 3, + 7 + ], + "api": [ + 2 + ], + "h": 360, + "w": 640, + "maxextended": -1, + "boxingallowed": 1, + "playbackmethod": [ + 3 + ], + "playbackend": 1, + "pos": 1, + "playerSize": [ + [ + 640, + 360 + ] + ], + "context": "instream", + "placement": 1, + "startdelay": 0 + } + }, + "adUnitCode": "div-gpt-ad-51545-0", + "transactionId": null, + "adUnitId": "b88648c1-fb3c-475e-bc44-764d12dbf4d8", + "sizes": [ + [ + 640, + 360 + ] + ], + "bidId": "8d37414a-7a4f-4f3b-a922-5e9db77a6d6c", + "bidderRequestId": "ba7c3a68-9f32-4fab-97dc-d016fcef290b", + "auctionId": null, + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "ref": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "content": { + "url": "" + } + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + } + }], + "auctionStart": 1759252766012, + "timeout": 3000, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "topmostLocation": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "location": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "canonicalUrl": null, + "page": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "domain": "test.localhost:9999", + "ref": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "canonicalUrl": null + } + }, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "ref": "http://test.localhost:9999/integrationExamples/videoModule/videojs/localVideoCache.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member_id=13859", + "content": { + "url": "" + } + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + }, + "start": 1759252766017 + }; + + const videoInstreamBidResponse = { + "body": { + "id": "e999d11a-38f8-46e3-84ec-55103f10e760", + "seatbid": [{ + "bid": [{ + "id": "6400954803477699288", + "impid": "8d37414a-7a4f-4f3b-a922-5e9db77a6d6c", + "price": 10, + "adid": "484626808", + "adm": "adnxs", + "adomain": [ + "example.com" + ], + "iurl": "https://nym2-ib.adnxs.com/cr?id=484626808", + "nurl": "https://nym2-ib.adnxs.com/something?", + "cid": "13859", + "crid": "484626808", + "h": 1, + "w": 1, + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 3335251835858264600, + "bidder_id": 2, + "bid_ad_type": 1, + "creative_info": { + "video": { + "duration": 30, + "mimes": [ + "video/mp4" + ] + } + }, + "buyer_line_item_id": 0, + "seller_line_item_id": 0, + "curator_line_item_id": 0, + "advertiser_id": 6621028, + "renderer_id": 0, + "no_ad_url": "https://nym2-ib.adnxs.com/it?an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2FvideoModule%2Fvideojs%2FlocalVideoCache.html%3Fpbjs_debug%3Dtrue%26apn_debug_dongle%3DQWERTY%26apn_debug_member_id%3D13859&e=wqT_3QLTDKBTBgAAAwDWAAUBCM-i8MYGEPvDtIb7u8ykLhjGpKbEk569pU4qNgkAAAkCABEJBwgAABkJCQjgPyEJCQgAACkRCQAxCQnwdeA_MLGGhA84o2xAo2xIAFAAWJWvogFgAGidjcYBeACAAQGKAQCSAQNVU0SYAQGgAQGoAQGwAQC4AQPAAQDIAQLQAQDYAQDgAQDwAQCKAj51ZignYScsIDY2MjEwMjgsIDApO3VmKCdpJywgNzY1ODIzNywgMCkFFDByJywgNDg0NjI2ODA4BRbwi5ICuQQhcUdYT1pnajhySVFjRVBpaWktY0JHQUFnbGEtaUFUQUFPQUJBQkVpamJGQ3hob1FQV0FCZ3pnRm9BSEFBZUFDQUFRQ0lBUUNRQVFHWUFRR2dBUUdvQVFHd0FRQzVBWlVjNEVBMFA0OUF3UUh6cldxa0FBQWtRTWtCQUFBQUFBQUE4RF9aQVFBCQ50UEFfNEFIOXRkTUQ5UUVBQUNCQm1BSUFvQUlCdFFJBSQAdg0I8FV3QUlBeUFJQTBBSUEyQUlBNEFJQTZBSUEtQUlBZ0FNQm1BTUJ1Z01KVGxsTk1qbzBOVFkwNEFPcVNvQUU2TG1yQ1lnRWh2VGxESkFFQUpnRUFjRUVBQQVjFEFBQURKQgEHDQEYMkFRQThRUQ0OKEFBQUlnRjFDT3BCERMUUEFfc1FVARoJAQhNRUYJCRRBQUpFREoVKAxBQUEwBSggQkFNei1QUU5rFShIOERfZ0JjQ0VQZkFGNUk2b0NfZwEIYFVBNElHQTFWVFJJZ0dBSkFHQVpnR0FLRUcBTAEBLEpFQ29CZ1N5QmlRSgEQDQEAUg0IAQEAWgEFDQEAaA0IgEFBQUM0QmlqNEI3alRDUGdIb3RVSS1BZnQzQWo0QjZqcwEUGDhfd0lnUWcBLQEBXGtRSWdJQUpBSUFBLi6aApkBIWhoR1U5dzo9AixKV3ZvZ0VnQkNnQU0xIVRDUkFPZ2xPV1UweU9qUTFOalJBcWtwFbEIOEQ5HbEAQh2xAEIdsQRCcAGGCQEEQngJCAEBEEI0QUlrNbDwUjhEOC7YAgDgAuSPXeoCmQFodHRwOi8vdGVzdC5sb2NhbGhvc3Q6OTk5OS9pbnRlZ3JhdGlvbkV4YW1wbGVzL3ZpZGVvTW9kdWxlL3ZpZGVvanMvBTeIVmlkZW9DYWNoZS5odG1sP3BianNfZGVidWc9dHJ1ZSZhcG4JDzRfZG9uZ2xlPVFXRVJUWR0Y9CoBbWVtYmVyX2lkPTEzODU5gAMAiAMBkAMAmAMUoAMBqgMCSADAA-CoAcgDANgDAOADAOgDAPgDA4AEAJIEEi9vcGVucnRiMi9wcmViaWRqc5gEAaIEDjEwMC4xNC4xNjMuMjUwqASORbIEEggBEAgYgAUg6AIoAjAAOARCALgEAMAEAMgEANIEDzEzODU5I05ZTTI6NDU2NNoEAggA4AQA8AT4oovnAYgFAZgFAKAF____________AaoFJGU5OTlkMTFhLTM4ZjgtNDZlMy04NGVjLTU1MTAzZjEwZTc2MMAFAMkFAAAAAAAA8D_SBQkJAAAAAAAAAADYBQHgBQHwBQH6BQQIABAAkAYBmAYAuAYAwQYAAAAAAADwv9AG2OYD2gYWChAAAAAAAAABRQkBbBAAGADgBgTyBgIIAIAHAYgHAKAHQMgHANIHDwkJIgAABSQUIADaBwYIBQvwk-AHAOoHAggA8AeQ9aYDighhCl0AAAGZm6OcmC5JMd-wzSH7S9CRaxvHzflX566DLUSOdQz88wyj3PqZEziVi4kwgLfD1XTpdj9BkTddNkqxU3TRdaKoURBAeRFiz3Ky5sh4Ali0fl6qRX1x8G-p788QAZUIAACAP5gIAcAIANIIBggAEAAYANoIBAgAIADgCADoCAA.&s=20f85682f8ef5755702e4b1bc90549390e5b580a", + "asset_url": "https://nym2-ib.adnxs.com/ab?ro=1&an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2FvideoModule%2Fvideojs%2FlocalVideoCache.html%3Fpbjs_debug%3Dtrue%26apn_debug_dongle%3DQWERTY%26apn_debug_member_id%3D13859&e=wqT_3QLpDqBpBwAAAwDWAAUBCM-i8MYGEPvDtIb7u8ykLhjGpKbEk569pU4qNgkAAAECCCRAEQEHEAAAJEAZCQkI4D8hCQkIJEApEQkAMQkJsOA_MLGGhA84o2xAo2xIAlD4oovnAViVr6IBYABonY3GAXgAgAEBigEDVVNEkgUG8EaYAQGgAQGoAQGwAQC4AQPAAQTIAQLQAQDYAQDgAQDwAQCKAj51ZignYScsIDY2MjEwMjgsIDApO3VmKCdpJywgNzY1ODIzNxUUMHInLCA0ODQ2MjY4MDgFFvCLkgK5BCFxR1hPWmdqOHJJUWNFUGlpaS1jQkdBQWdsYS1pQVRBQU9BQkFCRWlqYkZDeGhvUVBXQUJnemdGb0FIQUFlQUNBQVFDSUFRQ1FBUUdZQVFHZ0FRR29BUUd3QVFDNUFaVWM0RUEwUDQ5QXdRSHpyV3FrQUFBa1FNa0JBQUFBQUFBQThEX1pBUUEJDnRQQV80QUg5dGRNRDlRRUFBQ0JCbUFJQW9BSUJ0UUkFJAB2DQjwVXdBSUF5QUlBMEFJQTJBSUE0QUlBNkFJQS1BSUFnQU1CbUFNQnVnTUpUbGxOTWpvME5UWTA0QU9xU29BRTZMbXJDWWdFaHZUbERKQUVBSmdFQWNFRUFBBWMUQUFBREpCAQcNARgyQVFBOFFRDQ4oQUFBSWdGMUNPcEIRExRQQV9zUVUBGgkBCE1FRgkJFEFBSkVEShUoDEFBQTAFKCBCQU16LVBRTmsVKEg4RF9nQmNDRVBmQUY1STZvQ19nAQhgVUE0SUdBMVZUUklnR0FKQUdBWmdHQUtFRwFMAQEsSkVDb0JnU3lCaVFKARANAQBSDQgBAQBaAQUNAQBoDQiAQUFBQzRCaWo0QjdqVENQZ0hvdFVJLUFmdDNBajRCNmpzARQYOF93SWdRZwEtAQFca1FJZ0lBSkFJQUEuLpoCmQEhaGhHVTl3Oj0CLEpXdm9nRWdCQ2dBTTEhVENSQU9nbE9XVTB5T2pRMU5qUkFxa3AVsQg4RDkdsQBCHbEAQh2xBEJwAYYJAQRCeAkIAQEQQjRBSWs1sPBSOEQ4LtgCAOAC5I9d6gKZAWh0dHA6Ly90ZXN0LmxvY2FsaG9zdDo5OTk5L2ludGVncmF0aW9uRXhhbXBsZXMvdmlkZW9Nb2R1bGUvdmlkZW9qcy8FN4hWaWRlb0NhY2hlLmh0bWw_cGJqc19kZWJ1Zz10cnVlJmFwbgkPNF9kb25nbGU9UVdFUlRZHRhwbWVtYmVyX2lkPTEzODU58gIRCgZBRFZfSUQSBzZpwhzyAhIKBkNQRwEUPAgyMzcyNTkyNPICCgoFQ1ABFBgBMPICDQoIATYMRlJFUREQHFJFTV9VU0VSBRAADAkgGENPREUSAPIBDwFREQ8QCwoHQ1AVDhAQCgVJTwFZBAc3iS8A8gEhBElPFSE4EwoPQ1VTVE9NX01PREVMASsUAPICGgoWMhYAHExFQUZfTkFNBXEIHgoaNh0ACEFTVAE-EElGSUVEAT4cDQoIU1BMSVQBTfDlATCAAwCIAwGQAwCYAxSgAwGqAwJIAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQSL29wZW5ydGIyL3ByZWJpZGpzmAQBogQOMTAwLjE0LjE2My4yNTCoBI5FsgQSCAEQCBiABSDoAigCMAA4BEIAuAQAwAQAyAQA0gQPMTM4NTkjTllNMjo0NTY02gQCCAHgBADwBPiii-cBiAUBmAUAoAX___________8BqgUkZTk5OWQxMWEtMzhmOC00NmUzLTg0ZWMtNTUxMDNmMTBlNzYwwAUAyQUAAAAAAADwP9IFCQkAAAAFDmjYBQHgBQHwBQH6BQQIABAAkAYBmAYAuAYAwQYFIDAA8D_QBtjmA9oGFgoQCRIZAWwQABgA4AYE8gYCCACABwGIBwCgB0DIBwDSBw8JESYBJBAgANoHBgFe8J0YAOAHAOoHAggA8AeQ9aYDighhCl0AAAGZm6OcmC5JMd-wzSH7S9CRaxvHzflX566DLUSOdQz88wyj3PqZEziVi4kwgLfD1XTpdj9BkTddNkqxU3TRdaKoURBAeRFiz3Ky5sh4Ali0fl6qRX1x8G-p788QAZUIAACAP5gIAcAIANIIDgiBgoSIkKDAgAEQABgA2ggECAAgAOAIAOgIAA..&s=ec6b67f896520314ab0b7fdb4b0847a14df44537{AUCTION_PRICE}" + } + } + }], + "seat": "13859" + }], + "bidid": "3531514400060956584", + "cur": "USD", + "ext": { + "tmaxrequest": 150 + } + }, + "headers": {} + }; + + const videoOutstreamBidderRequest = { + "bidderCode": "msft", + "auctionId": null, + "bidderRequestId": "3348a473-ad23-4672-bd82-cb0625b1ccd5", + "bids": [{ + "bidder": "msft", + "params": { + "placement_id": 33911093, + "video": { + "skippable": true + } + }, + "ortb2Imp": { + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 640, + "h": 480, + "plcmt": 4 + }, + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "/19968336/prebid_outstream_adunit_1" + } + }, + "gpid": "/19968336/prebid_outstream_adunit_1" + } + }, + "mediaTypes": { + "video": { + "playerSize": [ + [ + 640, + 480 + ] + ], + "context": "outstream", + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "plcmt": 4, + "w": 640, + "h": 480 + } + }, + "adUnitCode": "video1", + "transactionId": null, + "adUnitId": "202e3ff9-e9fc-4b91-84d8-c808e7f8f1b2", + "sizes": [ + [ + 640, + 480 + ] + ], + "bidId": "29ffa2b1-821d-4542-b948-8533c1832a68", + "bidderRequestId": "3348a473-ad23-4672-bd82-cb0625b1ccd5", + "auctionId": null, + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 815 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Google Chrome", + "version": [ + "141" + ] + }, + { + "brand": "Not?A_Brand", + "version": [ + "8" + ] + }, + { + "brand": "Chromium", + "version": [ + "141" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + } + }], + "auctionStart": 1759325217458, + "timeout": 3000, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "topmostLocation": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true", + "location": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true", + "canonicalUrl": null, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true", + "domain": "test.localhost:9999", + "ref": null, + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true", + "canonicalUrl": null + } + }, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/outstream.html?pbjs_debug=true" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 815 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Google Chrome", + "version": [ + "141" + ] + }, + { + "brand": "Not?A_Brand", + "version": [ + "8" + ] + }, + { + "brand": "Chromium", + "version": [ + "141" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + }, + "start": 1759325217463 + } + + const videoOutstreamBidResponse = { + "body": { + "id": "cb624440-f8bd-4da1-8256-d8a243651bef", + "seatbid": [{ + "bid": [{ + "id": "3757141233787776626", + "impid": "29ffa2b1-821d-4542-b948-8533c1832a68", + "price": 25.00001, + "adid": "546521568", + "adm": "adnxs", + "adomain": [ + "example.com" + ], + "iurl": "https://nym2-ib.adnxs.com/cr?id=546521568", + "nurl": "https://nym2-ib.adnxs.com/something", + "cid": "7877", + "crid": "546521568", + "h": 1, + "w": 1, + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 9005203323134521000, + "bidder_id": 2, + "bid_ad_type": 1, + "creative_info": { + "video": { + "duration": 30, + "mimes": [ + "video/mp4", + "video/webm" + ] + } + }, + "buyer_line_item_id": 0, + "seller_line_item_id": 0, + "curator_line_item_id": 0, + "advertiser_id": 10896419, + "renderer_id": 2, + "renderer_url": "https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js", + "renderer_config": "{}", + "no_ad_url": "https://nym2-ib.adnxs.com/it?an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2Fgpt%2Fold%2Foutstream.html%3Fpbjs_debug%3Dtrue&e=wqT_3QKJDHwJBgAAAwDWAAUBCK_Y9MYGEJPj5qzflrr8fBgAKjYJAA0BABENCAQAGQkJCOA_IQkJCAAAKREJADEJCfSoAeA_MLXilRA4xT1AxT1IAFAAWIKjsQFgAGjIgtUBeACAAQGKAQCSAQNVU0SYAQGgAQGoAQGwAQC4AQPAAQDIAQLQAQDYAQDgAQDwAQCKAkB1ZignYScsIDEwODk2NDE5LCAwKTt1ZignaScsIDEwNTkyNDIwLCAwKTt1ZigncicsIDU0NjUyMTU2OCwgMCk7kgK9BCEyMmNxTndpcHI3a2RFT0NEellRQ0dBQWdncU94QVRBQU9BQkFCRWpGUFZDMTRwVVFXQUJnX19fX193OW9BSEFXZUFDQUFSYUlBUUNRQVFHWUFRR2dBUUdvQVFHd0FRQzVBWEJaaGMwQUFEbEF3UUZ3V1lYTkFBQTVRTWtCQUFBQUFBQUE4RF9aQVFBQUFBQUFBUEFfNEFHa3dZWUY5UUVBQU1oQm1BSUFvQUlCdFFJQUFBQUF2UUlBQUFBQXdBSUJ5QUlCMEFJVzJBSUE0QUlBNkFJQS1BSUFnQU1CbUFNQnVnTUpUbGxOTWpvME9UUXg0QU9yU29BRWdObUtENGdFdy1DS0Q1QUVBSmdFQWNFRUFBQUFBAYgIQURKFaEkQUFBMkFRQThRUQELCQEcSWdGelNhcEIRExRQQV9zUVUJHAEBCE1FRgEHAQEMT1VESi4oAAAwLigABE5rFSjIOERfZ0JhSExtQUh3QllQdTNRejRCYU9JbVFXQ0JnTlZVMFNJQmdDUUJnR1lCZ0NoQmdBAV80QUFEbEFxQVlFc2dZa0MRkAxBQUFFHQwARx0MAEkdDKB1QVlVLUFlNDB3ajRCNkxWQ1BnSDdkd0ktQWVvN0FqNEJfUDhDSUVJQQFRaEFBQU9VQ0lDQUNRQ0FBLpoCmQEhR2hHSHlnaTZBAixJS2pzUUVnQkNnQU0RbVhEbEFPZ2xPV1UweU9qUTVOREZBcTBwSgFVAQEMOEQ5UgEICQEEQloJCAEBBEJoAQYJAQRCcAkIAQEEQngBBgkBEEI0QUlrNbD0NAE4RDgu2AIA4ALUxj3qAlVodHRwOi8vdGVzdC5sb2NhbGhvc3Q6OTk5OS9pbnRlZ3JhdGlvbkV4YW1wbGVzL2dwdC9vbGQvb3V0c3RyZWFtLmh0bWw_cGJqc19kZWJ1Zz10cnVlgAMAiAMBkAMAmAMUoAMBqgMCSADAA-CoAcgDANgDAOADAOgDAPgDA4AEAJIEEi9vcGVucnRiMi9wcmViaWRqc5gEAKIEDjEwMC4xNC4xNjMuMjUwqAQAsgQQCAAQABiABSDgAzAAOARCALgEAMAEAMgEANIEDjc4NzcjTllNMjo0OTQx2gQCCADgBADwBOCDzYQCiAUBmAUAoAX___________8BqgUkY2I2MjQ0NDAtZjhiZC00ZGExLTgyNTYtZDhhMjQzNjUxYmVmwAUAyQWJpBDwP9IFCZXdaNgFAeAFAfAFAfoFBAgAEACQBgGYBgC4BgDBBg0vJL_QBqIo2gYWChAJERkBcBAAGADgBgTyBgIIAIAHAYgHAKAHQMgHybsF0gcPFWIBJhAgANoHBgFf8IEYAOAHAOoHAggA8AeQ9aYDighhCl0AAAGZn_SXmHz46LX1mbGTFPjBc4ofoClrarilv48ccB0T3Vm-FTukoSSDehJCIeSY21q6N-oSr0ocUA3idwnaOplNcuHDF9VJLxBvM58E-tcQVhuo1F41W8_LM1AQAZUIAACAP5gIAcAIANIIDYcw2ggECAAgAOAIAOgIAA..&s=ce270f0cb1dee88fbb6b6bb8d59b1d9ca7e38e90" + } + } + }], + "seat": "7877" + }], + "bidid": "1510787988993274243", + "cur": "USD", + "ext": { + "tmaxrequest": 150 + } + }, + "headers": {} + } + + const nativeBidderRequest = { + "bidderCode": "msft", + "auctionId": null, + "bidderRequestId": "cdfd0842-275b-4f87-8b46-8f4052454a5e", + "bids": [{ + "bidder": "msft", + "params": { + "placement_id": 33907873 + }, + "ortb2Imp": { + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "/19968336/prebid_native_cdn_test_1" + } + }, + "gpid": "/19968336/prebid_native_cdn_test_1" + } + }, + "nativeParams": { + "ortb": { + "ver": "1.2", + "assets": [{ + "id": 1, + "required": 1, + "img": { + "type": 3, + "w": 989, + "h": 742 + } + }, + { + "id": 2, + "required": 1, + "title": { + "len": 100 + } + }, + { + "id": 3, + "required": 1, + "data": { + "type": 1 + } + } + ] + } + }, + "nativeOrtbRequest": { + "ver": "1.2", + "assets": [{ + "id": 1, + "required": 1, + "img": { + "type": 3, + "w": 989, + "h": 742 + } + }, + { + "id": 2, + "required": 1, + "title": { + "len": 100 + } + }, + { + "id": 3, + "required": 1, + "data": { + "type": 1 + } + } + ] + }, + "mediaTypes": { + "native": { + "ortb": { + "ver": "1.2", + "assets": [{ + "id": 1, + "required": 1, + "img": { + "type": 3, + "w": 989, + "h": 742 + } + }, + { + "id": 2, + "required": 1, + "title": { + "len": 100 + } + }, + { + "id": 3, + "required": 1, + "data": { + "type": 1 + } + } + ] + } + } + }, + "adUnitCode": "/19968336/prebid_native_cdn_test_1", + "transactionId": null, + "adUnitId": "e93238c6-03b8-4142-bd2b-af384da2b0ae", + "sizes": [], + "bidId": "519ca815-b76b-4ab0-9dc5-c78fa77dd7b1", + "bidderRequestId": "cdfd0842-275b-4f87-8b46-8f4052454a5e", + "auctionId": null, + "src": "client", + "auctionsCount": 1, + "bidRequestsCount": 1, + "bidderRequestsCount": 1, + "bidderWinsCount": 0, + "deferBilling": false, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "ref": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + } + }], + "auctionStart": 1759249513048, + "timeout": 3000, + "refererInfo": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "topmostLocation": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "location": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "canonicalUrl": null, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "domain": "test.localhost:9999", + "ref": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "legacy": { + "reachedTop": true, + "isAmp": false, + "numIframes": 0, + "stack": [], + "referer": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "canonicalUrl": null + } + }, + "ortb2": { + "site": { + "domain": "test.localhost:9999", + "publisher": { + "domain": "test.localhost:9999" + }, + "page": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325", + "ref": "http://test.localhost:9999/integrationExamples/gpt/old/demo_native_cdn.html?pbjs_debug=true&apn_debug_dongle=QWERTY&apn_debug_member=9325" + }, + "device": { + "w": 2560, + "h": 1440, + "dnt": 0, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", + "language": "en", + "ext": { + "vpw": 2560, + "vph": 647 + }, + "sua": { + "source": 1, + "platform": { + "brand": "macOS" + }, + "browsers": [{ + "brand": "Chromium", + "version": [ + "140" + ] + }, + { + "brand": "Not=A?Brand", + "version": [ + "24" + ] + }, + { + "brand": "Google Chrome", + "version": [ + "140" + ] + } + ], + "mobile": 0 + } + }, + "source": {} + }, + "start": 1759249513055 + }; + + const nativeBidResponse = { + "body": { + "id": "408873b5-0b75-43f2-b490-ba05466265e7", + "seatbid": [{ + "bid": [{ + "id": "2634147710021988035", + "impid": "519ca815-b76b-4ab0-9dc5-c78fa77dd7b1", + "price": 5, + "adid": "546255182", + "adm": "{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"img\":{\"url\":\"https:\\/\\/crcdn01.adnxs-simple.com\\/creative20\\/p\\/9325\\/2024\\/8\\/14\\/60018074\\/6ceb0f95-1465-4e90-b295-4b6e2aff3035.jpg\",\"w\":989,\"h\":742,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":2,\"title\":{\"text\":\"This is a AST Native Creative\"}},{\"id\":3,\"data\":{\"value\":\"AST\"}}],\"link\":{\"url\":\"https:\\/\\/nym2-ib.adnxs.com\\/click2?e=wqT_3QKfAfBDnwAAAAMAxBkFAQizifDGBhD8vNTpipOK8TEYxqSmxJOevaVOIKHJlRAo7Ugw7Ug4AkDO4ryEAkijlKIBUABaA1VTRGIBBWhoAXABeJ7txQGAAQCIAQGQAQKYAQSgAQKpAQAFAQgUQLEVCgC5DQoI4D_BDQoIFEDJFQow2AEA4AEA8AH1L_gBAA..\\/s=fec918f3f3660ce11dc2975bb7beb9df3d181748\\/bcr=AAAAAAAA8D8=\\/pp=${AUCTION_PRICE}\\/cnd=%21khGz_Qi-7rgdEM7ivIQCGKOUogEgBCgAMQAAAAAAABRAOglOWU0yOjQ5ODlAqkpJAAAAAAAA8D9RAAAAAAAAAABZAAAAAAAAAABhAAAAAAAAAABpAAAAAAAAAABxAAAAAAAAAAB4AIkBAAAAAAAA8D8.\\/cca=OTMyNSNOWU0yOjQ5ODk=\\/bn=0\\/clickenc=http%3A%2F%2Fprebid.org\"},\"eventtrackers\":[{\"event\":1,\"method\":1,\"url\":\"https:\\/\\/nym2-ib.adnxs.com\\/it?an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2Fgpt%2Fold%2Fdemo_native_cdn.html%3Fpbjs_debug%3Dtrue%26apn_debug_dongle%3DQWERTY%26apn_debug_member%3D9325&e=wqT_3QLSDKBSBgAAAwDWAAUBCLOJ8MYGEPy81OmKk4rxMRjGpKbEk569pU4qNgkAAAECCBRAEQEHEAAAFEAZCQkI4D8hCQkIFEApEQkAMQkJsOA_MKHJlRA47UhA7UhIAlDO4ryEAlijlKIBYABonu3FAXgAgAEBigEDVVNEkgUG8EaYAQGgAQGoAQGwAQC4AQLAAQTIAQLQAQDYAQDgAQDwAQCKAj51ZignYScsIDY1NjgyOTEsIDApO3VmKCdpJywgNzYyNDE2NRUUMHInLCA1NDYyNTUxODIFFvCQkgK9BCF3Mmd1QmdpLTdyZ2RFTTdpdklRQ0dBQWdvNVNpQVRBQU9BQkFCRWp0U0ZDaHlaVVFXQUJnemdGb0FIQU9lTFlZZ0FFT2lBRzJHSkFCQVpnQkFhQUJBYWdCQWJBQkFMa0I4NjFxcEFBQUZFREJBZk90YXFRQUFCUkF5UUVBQUFBQUFBRHdQOWtCQUFBQQEPdDhEX2dBZVdyMFFQMUFRQUFvRUNZQWdDZ0FnRzFBZwEiBEM5CQjwVURBQWdESUFnRFFBZzdZQXJZWTRBSUE2QUlBLUFJQWdBTUJtQU1CdWdNSlRsbE5Nam8wT1RnNTRBT3FTb0FFdXM2Z0NZZ0VrZnFKRDVBRUFKZ0VBY0VFAWIJAQREShWVJEFBQTJBUUE4UVEBCwkBHElnRl9TYXBCERMUUEFfc1FVCRwBAQhNRUYBBwEBDEZFREouKAAAMC4oAAROaxUoAfywQmFEQ0h2QUYyYXZkRFBnRjRfS1FBNElHQTFWVFJJZ0dBSkFHQVpnR0FLRUdBAV0lXCRDb0JnU3lCaVFKARANAQBSDQgBAQBaAQUNAQBoDQiAQUFBQzRCZ3I0QjdqVENQZ0hvdFVJLUFmdDNBajRCNmpzARQUOF93SWdRJXkBAVxVUUlnSUFKQUlBQS4umgKZASFraEd6X1E6QQIsS09Vb2dFZ0JDZ0FNMSFUQlJBT2dsT1dVMHlPalE1T0RsQXFrcBWxCDhEOR2xAEIdsQBCHbEEQnABhgkBBEJ4CQgBARBCNEFJazWw9LYBOEQ4LtgC9-kD4AKtmEjqAokBaHR0cDovL3Rlc3QubG9jYWxob3N0Ojk5OTkvaW50ZWdyYXRpb25FeGFtcGxlcy9ncHQvb2xkL2RlbW9fbmF0aXZlX2Nkbi5odG1sP3BianNfZGVidWc9dHJ1ZSZhcG5fZGVidWdfZG9uZ2xlPVFXRVJUWSZhcG5fZGVidWdfbWVtYmVyPTkzMjWAAwCIAwGQAwCYAxSgAwGqAwJIAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQSL29wZW5ydGIyL3ByZWJpZGpzmAQAogQOMTAwLjE0LjE2My4yNTCoBNhEsgQOCAAQABgAIAAwADgAQgC4BADABADIBADSBA45MzI1I05ZTTI6NDk4OdoEAggB4AQA8ATO4ryEAogFAZgFAKAF____________AaoFJDQwODg3M2I1LTBiNzUtNDNmMi1iNDkwLWJhMDU0NjYyNjVlN8AFAMkFAAAAAAAA8D_SBQkJAAAAAAAAAADYBQHgBQHwBQH6BQQIABAAkAYBmAYAuAYAwQYAAAAAAADwP9AG9S_aBhYKEAAAAAAAAAAAAAAAAAEbaBAAGADgBgzyBgIIAIAHAYgHAKAHQcgHANIHDxVgASQQIADaBwYJ8vCb4AcA6gcCCADwB5D1pgOKCGEKXQAAAZmbcls4MeIomK01HnxjZ19jnODCYNG_e0eXMrsJyOA5um4JVppxvM9079B8pwi2cU2gbzDjYgmYgkdUJXwe4yn9EtYSYNavJIDFeQm0RRGvDEj6ltcLGUilABABlQgAAIA_mAgBwAgA0ggOCIGChIiQoMCAARAAGADaCAQIACAA4AgA6AgA&s=0a66129aafb703cfab8bbce859eacdfa0f456a28&pp=${AUCTION_PRICE}\"}]}", + "adomain": [ + "example.com" + ], + "iurl": "https://nym2-ib.adnxs.com/cr?id=546255182", + "cid": "9325", + "crid": "546255182", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 3594480088801156600, + "bidder_id": 2, + "bid_ad_type": 3, + "buyer_line_item_id": 0, + "seller_line_item_id": 0, + "curator_line_item_id": 0, + "advertiser_id": 6568291, + "renderer_id": 0, + "no_ad_url": "https://nym2-ib.adnxs.com/it?an_audit=0&referrer=http%3A%2F%2Ftest.localhost%3A9999%2FintegrationExamples%2Fgpt%2Fold%2Fdemo_native_cdn.html%3Fpbjs_debug%3Dtrue%26apn_debug_dongle%3DQWERTY%26apn_debug_member%3D9325&e=wqT_3QLDDKBDBgAAAwDWAAUBCLOJ8MYGEPy81OmKk4rxMRjGpKbEk569pU4qNgkAAAkCABEJBwgAABkJCQjgPyEJCQgAACkRCQAxCQnwdeA_MKHJlRA47UhA7UhIAFAAWKOUogFgAGie7cUBeACAAQGKAQCSAQNVU0SYAQGgAQGoAQGwAQC4AQLAAQDIAQLQAQDYAQDgAQDwAQCKAj51ZignYScsIDY1NjgyOTEsIDApO3VmKCdpJywgNzYyNDE2NSwgMCkFFDByJywgNTQ2MjU1MTgyBRbwkJICvQQhdzJndUJnaS03cmdkRU03aXZJUUNHQUFnbzVTaUFUQUFPQUJBQkVqdFNGQ2h5WlVRV0FCZ3pnRm9BSEFPZUxZWWdBRU9pQUcyR0pBQkFaZ0JBYUFCQWFnQkFiQUJBTGtCODYxcXBBQUFGRURCQWZPdGFxUUFBQlJBeVFFQUFBQUFBQUR3UDlrQkFBQUEBD3Q4RF9nQWVXcjBRUDFBUUFBb0VDWUFnQ2dBZ0cxQWcBIgRDOQkI8FVEQUFnRElBZ0RRQWc3WUFyWVk0QUlBNkFJQS1BSUFnQU1CbUFNQnVnTUpUbGxOTWpvME9UZzU0QU9xU29BRXVzNmdDWWdFa2ZxSkQ1QUVBSmdFQWNFRQFiCQEEREoVlSRBQUEyQVFBOFFRAQsJARxJZ0ZfU2FwQhETFFBBX3NRVQkcAQEITUVGAQcBAQxGRURKLigAADAuKAAETmsVKAH8sEJhRENIdkFGMmF2ZERQZ0Y0X0tRQTRJR0ExVlRSSWdHQUpBR0FaZ0dBS0VHQQFdJVwkQ29CZ1N5QmlRSgEQDQEAUg0IAQEAWgEFDQEAaA0IgEFBQUM0QmdyNEI3alRDUGdIb3RVSS1BZnQzQWo0QjZqcwEUFDhfd0lnUSV5AQFcVVFJZ0lBSkFJQUEuLpoCmQEha2hHel9ROkECLEtPVW9nRWdCQ2dBTTEhVEJSQU9nbE9XVTB5T2pRNU9EbEFxa3AVsQg4RDkdsQBCHbEAQh2xBEJwAYYJAQRCeAkIAQEQQjRBSWs1sPS2AThEOC7YAvfpA-ACrZhI6gKJAWh0dHA6Ly90ZXN0LmxvY2FsaG9zdDo5OTk5L2ludGVncmF0aW9uRXhhbXBsZXMvZ3B0L29sZC9kZW1vX25hdGl2ZV9jZG4uaHRtbD9wYmpzX2RlYnVnPXRydWUmYXBuX2RlYnVnX2RvbmdsZT1RV0VSVFkmYXBuX2RlYnVnX21lbWJlcj05MzI1gAMAiAMBkAMAmAMUoAMBqgMCSADAA-CoAcgDANgDAOADAOgDAPgDA4AEAJIEEi9vcGVucnRiMi9wcmViaWRqc5gEAKIEDjEwMC4xNC4xNjMuMjUwqATYRLIEDggAEAAYACAAMAA4AEIAuAQAwAQAyAQA0gQOOTMyNSNOWU0yOjQ5ODnaBAIIAOAEAPAEzuK8hAKIBQGYBQCgBf___________wGqBSQ0MDg4NzNiNS0wYjc1LTQzZjItYjQ5MC1iYTA1NDY2MjY1ZTfABQDJBQAAAAAAAPA_0gUJCQAAAAAAAAAA2AUB4AUB8AUB-gUECAAQAJAGAZgGALgGAMEGAAAAAAAA8L_QBvUv2gYWChAAAAAAAAAAAAAAAAABG2gQABgA4AYM8gYCCACABwGIBwCgB0HIBwDSBw8VYAEkECAA2gcGCfLwk-AHAOoHAggA8AeQ9aYDighhCl0AAAGZm3JbODHiKJitNR58Y2dfY5zgwmDRv3tHlzK7CcjgObpuCVaacbzPdO_QfKcItnFNoG8w42IJmIJHVCV8HuMp_RLWEmDWrySAxXkJtEURrwxI-pbXCxlIpQAQAZUIAACAP5gIAcAIANIIBggAEAAYANoIBAgAIADgCADoCAA.&s=98218facb1e5673b9630690b1a1b943ce1e978de" + } + } + }], + "seat": "9325" + }], + "bidid": "5186086519274374393", + "cur": "USD", + "ext": { + "tmaxrequest": 150 + } + }, + "headers": {} + }; + + it('should interpret a banner response', function () { + const request = spec.buildRequests(bannerBidderRequest.bids, bannerBidderRequest)[0]; + const bids = spec.interpretResponse(bannerBidResponse, request); + + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.mediaType).to.equal(BANNER); + expect(bid.cpm).to.equal(1.5); + expect(bid.ad).to.equal(bannerBidResponse.body.seatbid[0].bid[0].adm); + expect(bid.meta.advertiser_id).to.equal(2529885); + }); + + if (FEATURES.VIDEO) { + it('should interpret a video instream response', function () { + const request = spec.buildRequests(videoInstreamBidderRequest.bids, videoInstreamBidderRequest)[0]; + const bids = spec.interpretResponse(videoInstreamBidResponse, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.mediaType).to.equal(VIDEO); + expect(bid.cpm).to.equal(10); + expect(bid.vastUrl).to.equal(`${videoInstreamBidResponse.body.seatbid[0].bid[0].nurl}&redir=${encodeURIComponent(videoInstreamBidResponse.body.seatbid[0].bid[0].ext.appnexus.asset_url)}`); + expect(bid.playerWidth).to.equal(640); + expect(bid.playerHeight).to.equal(360); + expect(bid.meta.advertiser_id).to.equal(6621028); + }); + + it('should interpret a video outstream response', function () { + const request = spec.buildRequests(videoOutstreamBidderRequest.bids, videoOutstreamBidderRequest)[0]; + const bids = spec.interpretResponse(videoOutstreamBidResponse, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.mediaType).to.equal(VIDEO); + expect(bid.cpm).to.equal(25.00001); + expect(bid.vastXml).to.equal(videoOutstreamBidResponse.body.seatbid[0].bid[0].adm); + expect(bid.playerWidth).to.equal(640); + expect(bid.playerHeight).to.equal(480); + expect(bid.meta.advertiser_id).to.equal(10896419); + expect(typeof bid.renderer.render).to.equal('function'); + }); + } + + if (FEATURES.NATIVE) { + it('should interpret a native response', function () { + const request = spec.buildRequests(nativeBidderRequest.bids, nativeBidderRequest)[0]; + const bids = spec.interpretResponse(nativeBidResponse, request); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.mediaType).to.equal(NATIVE); + expect(bid.cpm).to.equal(5); + expect(bid.native.ortb.ver).to.equal('1.2'); + expect(bid.native.ortb.assets[0].id).to.equal(1); + expect(bid.native.ortb.assets[0].img.url).to.equal('https://crcdn01.adnxs-simple.com/creative20/p/9325/2024/8/14/60018074/6ceb0f95-1465-4e90-b295-4b6e2aff3035.jpg'); + expect(bid.native.ortb.assets[0].img.w).to.equal(989); + expect(bid.native.ortb.assets[0].img.h).to.equal(742); + expect(bid.native.ortb.assets[1].id).to.equal(2); + expect(bid.native.ortb.assets[1].title.text).to.equal('This is a AST Native Creative'); + expect(bid.native.ortb.assets[2].id).to.equal(3); + expect(bid.native.ortb.assets[2].data.value).to.equal('AST'); + expect(bid.native.ortb.eventtrackers[0].event).to.equal(1); + expect(bid.native.ortb.eventtrackers[0].method).to.equal(1); + expect(bid.native.ortb.eventtrackers[0].url).to.contains(['https://nym2-ib.adnxs.com/it']); + }); + } + }); + + describe('getUserSyncs', function () { + it('should return an iframe sync if enabled and GDPR consent is given', function () { + const syncOptions = { + iframeEnabled: true + }; + const gdprConsent = { + gdprApplies: true, + consentString: '...', + vendorData: { + purpose: { + consents: { + 1: true + } + } + } + }; + const syncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(syncs).to.deep.equal([{ + type: 'iframe', + url: 'https://acdn.adnxs.com/dmp/async_usersync.html' + }]); + }); + + it('should return a pixel sync if enabled', function () { + const syncOptions = { + pixelEnabled: true + }; + const syncs = spec.getUserSyncs(syncOptions, []); + expect(syncs).to.not.be.empty; + expect(syncs[0].type).to.equal('image'); + }); + }); +}); diff --git a/test/spec/modules/nativeryBidAdapter_spec.js b/test/spec/modules/nativeryBidAdapter_spec.js index e8706711b10..3aa4b90cba2 100644 --- a/test/spec/modules/nativeryBidAdapter_spec.js +++ b/test/spec/modules/nativeryBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec, converter } from 'modules/nativeryBidAdapter'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; +import * as ajax from 'src/ajax.js'; const ENDPOINT = 'https://hb.nativery.com/openrtb2/auction'; const MAX_IMPS_PER_REQUEST = 10; @@ -192,4 +193,102 @@ describe('NativeryAdapter', function () { logErrorSpy.restore(); }); }); + + describe('onBidWon callback', () => { + it('should exists and be a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should NOT call ajax when invalid or empty data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + spec.onBidWon(null); + spec.onBidWon({}); + spec.onBidWon(undefined); + expect(ajaxStub.called).to.be.false; + }); + it('should call ajax with correct payload when valid data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + const validData = { bidder: 'nativery', adUnitCode: 'div-1' }; + spec.onBidWon(validData); + assertTrackEvent(ajaxStub, 'NAT_BID_WON', validData) + }); + }); + + describe('onAdRenderSucceeded callback', () => { + it('should exists and be a function', () => { + expect(spec.onAdRenderSucceeded).to.exist.and.to.be.a('function'); + }); + it('should NOT call ajax when invalid or empty data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + spec.onAdRenderSucceeded(null); + spec.onAdRenderSucceeded({}); + spec.onAdRenderSucceeded(undefined); + expect(ajaxStub.called).to.be.false; + }); + it('should call ajax with correct payload when valid data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + const validData = { bidder: 'nativery', adUnitCode: 'div-1' }; + spec.onAdRenderSucceeded(validData); + assertTrackEvent(ajaxStub, 'NAT_AD_RENDERED', validData) + }); + }); + + describe('onTimeout callback', () => { + it('should exists and be a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should NOT call ajax when invalid or empty data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + spec.onTimeout(null); + spec.onTimeout({}); + spec.onTimeout([]); + spec.onTimeout(undefined); + expect(ajaxStub.called).to.be.false; + }); + it('should call ajax with correct payload when valid data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + const validData = [{ bidder: 'nativery', adUnitCode: 'div-1' }]; + spec.onTimeout(validData); + assertTrackEvent(ajaxStub, 'NAT_TIMEOUT', validData) + }); + }); + + describe('onBidderError callback', () => { + it('should exists and be a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should NOT call ajax when invalid or empty data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + spec.onBidderError(null); + spec.onBidderError({}); + spec.onBidderError(undefined); + expect(ajaxStub.called).to.be.false; + }); + it('should call ajax with correct payload when valid data is provided', () => { + const ajaxStub = sandBox.stub(ajax, 'ajax'); + const validData = { + error: 'error', + bidderRequest: { + bidder: 'nativery', + } + }; + spec.onBidderError(validData); + assertTrackEvent(ajaxStub, 'NAT_BIDDER_ERROR', validData) + }); + }); }); + +const assertTrackEvent = (ajaxStub, event, data) => { + expect(ajaxStub.calledOnce).to.be.true; + + const [url, callback, body, options] = ajaxStub.firstCall.args; + + expect(url).to.equal('https://hb.nativery.com/openrtb2/track-event'); + expect(callback).to.be.undefined; + expect(body).to.be.a('string'); + expect(options).to.deep.equal({ method: 'POST', withCredentials: true, keepalive: true }); + + const payload = JSON.parse(body); + expect(payload.event).to.equal(event); + expect(payload.prebidVersion).to.exist.and.to.be.a('string') + expect(payload.data).to.deep.equal(data); +} diff --git a/test/spec/modules/neuwoRtdProvider_spec.js b/test/spec/modules/neuwoRtdProvider_spec.js index c400eea0429..4b2951afe33 100644 --- a/test/spec/modules/neuwoRtdProvider_spec.js +++ b/test/spec/modules/neuwoRtdProvider_spec.js @@ -1,123 +1,1302 @@ -import { server } from 'test/mocks/xhr.js'; -import * as neuwo from 'modules/neuwoRtdProvider'; +import * as neuwo from "modules/neuwoRtdProvider"; +import { server } from "test/mocks/xhr.js"; + +const NEUWO_API_URL = "https://api.url.neuwo.ai/edge/GetAiTopics"; +const NEUWO_API_TOKEN = "token"; +const IAB_CONTENT_TAXONOMY_VERSION = "3.0"; -const PUBLIC_TOKEN = 'public_key_0000'; const config = () => ({ params: { - publicToken: PUBLIC_TOKEN, - apiUrl: 'https://testing-requirement.neuwo.api' - } -}) - -const apiReturns = () => ({ - somethingExtra: { object: true }, - marketing_categories: { - iab_tier_1: [ - { ID: 'IAB21', label: 'Real Estate', relevance: '0.45699' } - ] - } -}) - -const TAX_ID = '441' + neuwoApiUrl: NEUWO_API_URL, + neuwoApiToken: NEUWO_API_TOKEN, + iabContentTaxonomyVersion: IAB_CONTENT_TAXONOMY_VERSION, + }, +}); + +function getNeuwoApiResponse() { + return { + brand_safety: { + BS_score: "1.0", + BS_indication: "yes", + }, + marketing_categories: { + iab_tier_1: [ + { + ID: "274", + label: "Home & Garden", + relevance: "0.47", + }, + ], + iab_tier_2: [ + { + ID: "216", + label: "Cooking", + relevance: "0.41", + }, + ], + iab_tier_3: [], + iab_audience_tier_3: [ + { + ID: "49", + label: "Demographic | Gender | Female |", + relevance: "0.9923", + }, + ], + iab_audience_tier_4: [ + { + ID: "127", + label: "Demographic | Household Data | 1 Child |", + relevance: "0.9673", + }, + ], + iab_audience_tier_5: [ + { + ID: "98", + label: "Demographic | Household Data | Parents with Children |", + relevance: "0.9066", + }, + ], + }, + smart_tags: [ + { + ID: "123", + name: "animals-group", + }, + ], + }; +} +const CONTENT_TIERS = ["iab_tier_1", "iab_tier_2", "iab_tier_3"]; +const AUDIENCE_TIERS = ["iab_audience_tier_3", "iab_audience_tier_4", "iab_audience_tier_5"]; /** * Object generator, like above, written using alternative techniques * @returns object with predefined (expected) bidsConfig fields */ function bidsConfiglike() { - return Object.assign({}, { - ortb2Fragments: { global: {} } - }) + return Object.assign( + {}, + { + ortb2Fragments: { global: {} }, + } + ); } -describe('neuwoRtdProvider', function () { - describe('neuwoRtdModule', function () { - it('initializes', function () { - expect(neuwo.neuwoRtdModule.init(config())).to.be.true; - }) - it('init needs that public token', function () { - expect(neuwo.neuwoRtdModule.init()).to.be.false; - }) - - describe('segment picking', function () { - it('handles bad inputs', function () { - expect(neuwo.pickSegments()).to.be.an('array').that.is.empty; - expect(neuwo.pickSegments('technically also an array')).to.be.an('array').that.is.empty; - expect(neuwo.pickSegments({ bad_object: 'bad' })).to.be.an('array').that.is.empty; - }) - it('handles malformations', function () { - const result = neuwo.pickSegments([{something_wrong: true}, null, { ID: 'IAB19-20' }, { id: 'IAB3-1', ID: 'IAB9-20' }]) - expect(result[0].id).to.equal('631') - expect(result[1].id).to.equal('58') - expect(result.length).to.equal(2) - }) - }) - - describe('topic injection', function () { - it('mutates bidsConfig', function () { - const topics = apiReturns() - const bidsConfig = bidsConfiglike() - neuwo.injectTopics(topics, bidsConfig, () => { }) - expect(bidsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) - expect(bidsConfig.ortb2Fragments.global.site.content.data[0].segment[0].id, 'id of first segment in content.data').to.equal(TAX_ID) - expect(bidsConfig.ortb2Fragments.global.site.pagecat[0], 'category taxonomy code for pagecat').to.equal(TAX_ID) - }) - - it('handles malformed responses', function () { - let topics = { message: 'Forbidden' } - const bidsConfig = bidsConfiglike() - neuwo.injectTopics(topics, bidsConfig, () => { }) - expect(bidsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) - expect(bidsConfig.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; - - topics = '404 wouldn\'t really even show up for injection' - const bdsConfig = bidsConfiglike() - neuwo.injectTopics(topics, bdsConfig, () => { }) - expect(bdsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) - expect(bdsConfig.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; - - topics = undefined - const bdsConfigE = bidsConfiglike() - neuwo.injectTopics(topics, bdsConfigE, () => { }) - expect(bdsConfigE.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) - expect(bdsConfigE.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; - }) - }) - - describe('fragment addition', function () { - it('mutates input objects', function () { - const alphabet = { a: { b: { c: {} } } } - neuwo.addFragment(alphabet.a.b.c, 'd.e.f', { g: 'h' }) - expect(alphabet.a.b.c.d.e.f.g).to.equal('h') - }) - }) - - describe('getBidRequestData', function () { - it('forms requests properly and mutates input bidsConfig', function () { - const bids = bidsConfiglike() - const conf = config() +describe("neuwoRtdModule", function () { + beforeEach(function () { + // Clear the global cache before each test to ensure test isolation + neuwo.clearCache(); + }); + + describe("init", function () { + it("should return true when all required parameters are provided", function () { + expect( + neuwo.neuwoRtdModule.init(config()), + "should successfully initialize with a valid configuration" + ).to.be.true; + }); + + it("should return false when no configuration is provided", function () { + expect(neuwo.neuwoRtdModule.init(), "should fail initialization if config is missing").to.be + .false; + }); + + it("should return false when the neuwoApiUrl parameter is missing", function () { + const incompleteConfig = { + params: { + neuwoApiToken: NEUWO_API_TOKEN, + }, + }; + expect( + neuwo.neuwoRtdModule.init(incompleteConfig), + "should fail initialization if neuwoApiUrl is not set" + ).to.be.false; + }); + + it("should return false when the neuwoApiToken parameter is missing", function () { + const incompleteConfig = { + params: { + neuwoApiUrl: NEUWO_API_URL, + }, + }; + expect( + neuwo.neuwoRtdModule.init(incompleteConfig), + "should fail initialization if neuwoApiToken is not set" + ).to.be.false; + }); + }); + + describe("buildIabData", function () { + it("should return an empty segment array when no matching tiers are found", function () { + const marketingCategories = getNeuwoApiResponse().marketing_categories; + const tiers = ["non_existent_tier"]; + const segtax = 0; + const result = neuwo.buildIabData(marketingCategories, tiers, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [], + ext: { + segtax, + }, + }; + expect(result, "should produce a valid object with an empty segment array").to.deep.equal( + expected + ); + }); + + it("should correctly build the data object for content tiers", function () { + const marketingCategories = getNeuwoApiResponse().marketing_categories; + const segtax = 0; + const result = neuwo.buildIabData(marketingCategories, CONTENT_TIERS, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [ + { + id: "274", + name: "Home & Garden", + }, + { + id: "216", + name: "Cooking", + }, + ], + ext: { + segtax, + }, + }; + expect(result, "should aggregate segments from all specified content tiers").to.deep.equal( + expected + ); + }); + + it("should correctly build the data object for audience tiers", function () { + const marketingCategories = getNeuwoApiResponse().marketing_categories; + const segtax = 0; + const result = neuwo.buildIabData(marketingCategories, AUDIENCE_TIERS, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [ + { + id: "49", + name: "Demographic | Gender | Female |", + }, + { + id: "127", + name: "Demographic | Household Data | 1 Child |", + }, + { + id: "98", + name: "Demographic | Household Data | Parents with Children |", + }, + ], + ext: { + segtax, + }, + }; + expect(result, "should aggregate segments from all specified audience tiers").to.deep.equal( + expected + ); + }); + + it("should return an empty segment array when marketingCategories is null or undefined", function () { + const segtax = 4; + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [], + ext: { + segtax, + }, + }; + expect( + neuwo.buildIabData(null, CONTENT_TIERS, segtax), + "should handle null marketingCategories gracefully" + ).to.deep.equal(expected); + expect( + neuwo.buildIabData(undefined, CONTENT_TIERS, segtax), + "should handle undefined marketingCategories gracefully" + ).to.deep.equal(expected); + }); + + it("should return an empty segment array when marketingCategories is empty", function () { + const marketingCategories = {}; + const segtax = 4; + const result = neuwo.buildIabData(marketingCategories, CONTENT_TIERS, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [], + ext: { + segtax, + }, + }; + expect(result, "should handle an empty marketingCategories object").to.deep.equal(expected); + }); + + it("should gracefully handle if a marketing_categories key contains a non-array value", function () { + const marketingCategories = getNeuwoApiResponse().marketing_categories; + // Overwrite iab_tier_1 to be an object instead of an array + marketingCategories.iab_tier_1 = { ID: "274", label: "Home & Garden" }; + + const segtax = 4; + const result = neuwo.buildIabData(marketingCategories, CONTENT_TIERS, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + // The segment should only contain data from the valid iab_tier_2 + segment: [ + { + id: "216", + name: "Cooking", + }, + ], + ext: { + segtax, + }, + }; + + expect(result, "should skip non-array tier values and process valid ones").to.deep.equal( + expected + ); + }); + + it("should ignore malformed objects within a tier array", function () { + const marketingCategories = getNeuwoApiResponse().marketing_categories; + // Overwrite iab_tier_1 with various malformed objects + marketingCategories.iab_tier_1 = [ + { ID: "274", label: "Valid Object" }, + { ID: "999" }, // Missing 'label' property + { label: "Another Label" }, // Missing 'ID' property + null, // A null value + "just-a-string", // A string primitive + {}, // An empty object + ]; + + const segtax = 4; + const result = neuwo.buildIabData(marketingCategories, CONTENT_TIERS, segtax); + const expected = { + name: neuwo.DATA_PROVIDER, + // The segment should contain the one valid object from iab_tier_1 and the data from iab_tier_2 + segment: [ + { + id: "274", + name: "Valid Object", + }, + { + id: "216", + name: "Cooking", + }, + ], + ext: { + segtax, + }, + }; + + expect(result, "should filter out malformed entries within a tier array").to.deep.equal( + expected + ); + }); + + it("should return an empty segment array if the entire marketingCategories object is not a valid object", function () { + const segtax = 4; + const expected = { + name: neuwo.DATA_PROVIDER, + segment: [], + ext: { segtax }, + }; + // Test with a string + const resultString = neuwo.buildIabData("incorrect format", CONTENT_TIERS, segtax); + expect(resultString, "should handle non-object marketingCategories input").to.deep.equal( + expected + ); + }); + }); + + describe("injectOrtbData", function () { + it("should correctly mutate the request bids config object with new data", function () { + const reqBidsConfigObj = { ortb2Fragments: { global: {} } }; + neuwo.injectOrtbData(reqBidsConfigObj, "c.d.e.f", { g: "h" }); + expect( + reqBidsConfigObj.ortb2Fragments.global.c.d.e.f.g, + "should deeply merge the new data into the target object" + ).to.equal("h"); + }); + }); + + describe("getBidRequestData", function () { + describe("when using IAB Content Taxonomy 3.0", function () { + it("should correctly structure the bids object after a successful API response", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig = bidsConfiglike(); + const conf = config(); // control xhr api request target for testing - conf.params.argUrl = 'https://publisher.works/article.php?get=horrible_url_for_testing&id=5' + conf.params.websiteToAnalyseUrl = + "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; - neuwo.getBidRequestData(bids, () => { }, conf, 'any consent data works, clearly') + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should be a string").to.be.a("string"); + expect(request.url, "The request URL should include the public API token").to.include( + conf.params.neuwoApiToken + ); + expect(request.url, "The request URL should include the encoded website URL").to.include( + encodeURIComponent(conf.params.websiteToAnalyseUrl) + ); + + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + const contentData = bidsConfig.ortb2Fragments.global.site.content.data[0]; + expect(contentData.name, "The data provider name should be correctly set").to.equal( + neuwo.DATA_PROVIDER + ); + expect( + contentData.ext.segtax, + "The segtax value should correspond to IAB Content Taxonomy 3.0" + ).to.equal(7); + expect( + contentData.segment[0].id, + "The first segment ID should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_1[0].ID); + expect( + contentData.segment[1].name, + "The second segment name should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_2[0].label); + }); + }); + + describe("when using IAB Content Taxonomy 2.2", function () { + it("should correctly structure the bids object after a successful API response", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.iabContentTaxonomyVersion = "2.2"; + // control xhr api request target for testing + conf.params.websiteToAnalyseUrl = + "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); const request = server.requests[0]; - expect(request.url).to.be.a('string').that.includes(conf.params.publicToken) - expect(request.url).to.include(encodeURIComponent(conf.params.argUrl)) - request.respond(200, { 'Content-Type': 'application/json; encoding=UTF-8' }, JSON.stringify(apiReturns())); - - expect(bids.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) - expect(bids.ortb2Fragments.global.site.content.data[0].segment[0].id, 'id of first segment in content.data').to.equal(TAX_ID) - }) - - it('accepts detail not available result', function () { - const bidsConfig = bidsConfiglike() - const comparison = bidsConfiglike() - neuwo.getBidRequestData(bidsConfig, () => { }, config(), 'consensually') + + expect(request.url, "The request URL should be a string").to.be.a("string"); + expect(request.url, "The request URL should include the public API token").to.include( + conf.params.neuwoApiToken + ); + expect(request.url, "The request URL should include the encoded website URL").to.include( + encodeURIComponent(conf.params.websiteToAnalyseUrl) + ); + + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + const contentData = bidsConfig.ortb2Fragments.global.site.content.data[0]; + expect(contentData.name, "The data provider name should be correctly set").to.equal( + neuwo.DATA_PROVIDER + ); + expect( + contentData.ext.segtax, + "The segtax value should correspond to IAB Content Taxonomy 2.2" + ).to.equal(6); + expect( + contentData.segment[0].id, + "The first segment ID should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_1[0].ID); + expect( + contentData.segment[1].name, + "The second segment name should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_2[0].label); + }); + }); + + describe("when using the default IAB Content Taxonomy", function () { + it("should correctly structure the bids object after a successful API response", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.iabContentTaxonomyVersion = undefined; + // control xhr api request target for testing + conf.params.websiteToAnalyseUrl = + "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); const request = server.requests[0]; - request.respond(404, { 'Content-Type': 'application/json; encoding=UTF-8' }, JSON.stringify({ detail: 'Basically first time seeing this' })); - expect(bidsConfig).to.deep.equal(comparison) - }) - }) - }) -}) + + expect(request.url, "The request URL should be a string").to.be.a("string"); + expect(request.url, "The request URL should include the public API token").to.include( + conf.params.neuwoApiToken + ); + expect(request.url, "The request URL should include the encoded website URL").to.include( + encodeURIComponent(conf.params.websiteToAnalyseUrl) + ); + + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + const contentData = bidsConfig.ortb2Fragments.global.site.content.data[0]; + expect(contentData.name, "The data provider name should be correctly set").to.equal( + neuwo.DATA_PROVIDER + ); + expect( + contentData.ext.segtax, + "The segtax value should default to IAB Content Taxonomy 3.0" + ).to.equal(7); + expect( + contentData.segment[0].id, + "The first segment ID should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_1[0].ID); + expect( + contentData.segment[1].name, + "The second segment name should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_tier_2[0].label); + }); + }); + + describe("when using IAB Audience Taxonomy 1.1", function () { + it("should correctly structure the bids object after a successful API response", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig = bidsConfiglike(); + const conf = config(); + // control xhr api request target for testing + conf.params.websiteToAnalyseUrl = + "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should be a string").to.be.a("string"); + expect(request.url, "The request URL should include the public API token").to.include( + conf.params.neuwoApiToken + ); + expect(request.url, "The request URL should include the encoded website URL").to.include( + encodeURIComponent(conf.params.websiteToAnalyseUrl) + ); + + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + const userData = bidsConfig.ortb2Fragments.global.user.data[0]; + expect(userData.name, "The data provider name should be correctly set").to.equal( + neuwo.DATA_PROVIDER + ); + expect( + userData.ext.segtax, + "The segtax value should correspond to IAB Audience Taxonomy 1.1" + ).to.equal(4); + expect( + userData.segment[0].id, + "The first segment ID should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_audience_tier_3[0].ID); + expect( + userData.segment[1].name, + "The second segment name should match the API response" + ).to.equal(apiResponse.marketing_categories.iab_audience_tier_4[0].label); + }); + }); + + it("should not change the bids object structure after an unsuccessful API response", function () { + const bidsConfig = bidsConfiglike(); + const bidsConfigCopy = bidsConfiglike(); + const conf = config(); + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + request.respond( + 404, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify({ detail: "test error" }) + ); + expect( + bidsConfig, + "The bids config object should remain unmodified after a failed API call" + ).to.deep.equal(bidsConfigCopy); + }); + }); + + describe("cleanUrl", function () { + describe("when no stripping options are provided", function () { + it("should return the URL unchanged", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const result = neuwo.cleanUrl(url, {}); + expect(result, "should return the original URL with all query params intact").to.equal(url); + }); + + it("should return the URL unchanged when options object is empty", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url); + expect(result, "should handle missing options parameter").to.equal(url); + }); + }); + + describe("with query parameters edge cases", function () { + it("should strip all query parameters from the URL for `stripAllQueryParams` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripAllQueryParams: true }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page??"), "should remove double question mark").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + + it("should strip all query parameters from the URL for `stripQueryParamsForDomains` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripQueryParamsForDomains: ["example.com"] }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page??"), "should remove double question mark").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + + it("should strip all query parameters from the URL for `stripQueryParams` (edge cases)", function () { + const stripAll = (url) => neuwo.cleanUrl(url, { stripQueryParams: ["key", "key1", "key2", "", "?"] }); + const expected = "https://example.com/page"; + const expectedWithFragment = "https://example.com/page#anchor"; + + // Basic formats + expect(stripAll("https://example.com/page?key=value"), "should remove basic key=value params").to.equal(expected); + expect(stripAll("https://example.com/page?key="), "should remove params with empty value").to.equal(expected); + expect(stripAll("https://example.com/page?key"), "should remove params without equals sign").to.equal(expected); + expect(stripAll("https://example.com/page?=value"), "should remove params with empty key").to.equal(expected); + + // Multiple parameters + expect(stripAll("https://example.com/page?key1=value1&key2=value2"), "should remove multiple different params").to.equal(expected); + expect(stripAll("https://example.com/page?key=value1&key=value2"), "should remove multiple params with same key").to.equal(expected); + + // Special characters and encoding + expect(stripAll("https://example.com/page?key=value%20with%20spaces"), "should remove URL encoded spaces").to.equal(expected); + expect(stripAll("https://example.com/page?key=value+with+plus"), "should remove plus as space").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%3D%26%3F"), "should remove encoded special chars").to.equal(expected); + expect(stripAll("https://example.com/page?key=%"), "should remove incomplete encoding").to.equal(expected); + expect(stripAll("https://example.com/page?key=value%2"), "should remove malformed encoding").to.equal(expected); + + // Delimiters and syntax edge cases + expect(stripAll("https://example.com/page?&key=value"), "should remove params with leading ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&"), "should remove params with trailing ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value&&key2=value2"), "should remove params with double ampersand").to.equal(expected); + expect(stripAll("https://example.com/page?key=value?key2=value2"), "should remove params with question mark delimiter").to.equal(expected); + expect(stripAll("https://example.com/page?key=value;key2=value2"), "should remove params with semicolon delimiter").to.equal(expected); + + // Empty and missing cases + expect(stripAll("https://example.com/page?"), "should remove question mark alone").to.equal(expected); + expect(stripAll("https://example.com/page"), "should handle URL without query string").to.equal(expected); + + // Unicode and special values + expect(stripAll("https://example.com/page?key=值"), "should remove unicode characters").to.equal(expected); + expect(stripAll("https://example.com/page?key=null"), "should remove string 'null'").to.equal(expected); + expect(stripAll("https://example.com/page?key=undefined"), "should remove string 'undefined'").to.equal(expected); + + // Fragment positioning (fragments are preserved by default) + expect(stripAll("https://example.com/page?key=value#anchor"), "should remove query params and preserve fragment").to.equal(expectedWithFragment); + expect(stripAll("https://example.com/page#anchor?key=value"), "should preserve fragment before params").to.equal("https://example.com/page#anchor?key=value"); + }); + }); + + describe("when stripAllQueryParams is true", function () { + it("should strip all query parameters from the URL", function () { + const url = "https://example.com/page?foo=bar&baz=qux&test=123"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should remove all query parameters").to.equal(expected); + }); + + it("should return the URL unchanged if there are no query parameters", function () { + const url = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should handle URLs without query params").to.equal(url); + }); + + it("should preserve the hash fragment when stripping query params without stripFragments", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page#section"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should preserve hash fragments by default").to.equal(expected); + }); + + it("should strip hash fragment when stripFragments is enabled", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true, stripFragments: true }); + expect(result, "should strip both query params and fragments").to.equal(expected); + }); + + it("should strip query params but preserve path and protocol", function () { + const url = "https://subdomain.example.com:8080/path/to/page?param=value"; + const expected = "https://subdomain.example.com:8080/path/to/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true }); + expect(result, "should preserve protocol, domain, port, and path").to.equal(expected); + }); + }); + + describe("when stripQueryParamsForDomains is provided", function () { + it("should strip all query params for exact domain match", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should strip params for exact domain match").to.equal(expected); + }); + + it("should strip all query params for subdomain match", function () { + const url = "https://sub.example.com/page?foo=bar"; + const expected = "https://sub.example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should strip params for subdomains").to.equal(expected); + }); + + it("should not strip query params if domain does not match", function () { + const url = "https://other.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should preserve params for non-matching domains").to.equal(url); + }); + + it("should not strip query params if subdomain is provided for domain", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["sub.example.com"] + }); + expect(result, "should preserve params for domain when subdomain is provided").to.equal(url); + }); + + it("should handle multiple domains in the list", function () { + const url1 = "https://example.com/page?foo=bar"; + const url2 = "https://test.com/page?foo=bar"; + const url3 = "https://other.com/page?foo=bar"; + const domains = ["example.com", "test.com"]; + + const result1 = neuwo.cleanUrl(url1, { stripQueryParamsForDomains: domains }); + const result2 = neuwo.cleanUrl(url2, { stripQueryParamsForDomains: domains }); + const result3 = neuwo.cleanUrl(url3, { stripQueryParamsForDomains: domains }); + + expect(result1, "should strip params for first domain").to.equal("https://example.com/page"); + expect(result2, "should strip params for second domain").to.equal("https://test.com/page"); + expect(result3, "should preserve params for non-listed domain").to.equal(url3); + }); + + it("should handle deep subdomains correctly", function () { + const url = "https://deep.sub.example.com/page?foo=bar"; + const expected = "https://deep.sub.example.com/page"; + const result1 = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + const result2 = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["sub.example.com"] + }); + expect(result1, "should strip params for deep subdomains with domain matching").to.equal(expected); + expect(result2, "should strip params for deep subdomains with subdomain matching").to.equal(expected); + }); + + it("should not match partial domain names", function () { + const url = "https://notexample.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"] + }); + expect(result, "should not match partial domain strings").to.equal(url); + }); + + it("should handle empty domain list", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripQueryParamsForDomains: [] }); + expect(result, "should not strip params with empty domain list").to.equal(url); + }); + }); + + describe("when stripQueryParams is provided", function () { + it("should strip only specified query parameters", function () { + const url = "https://example.com/page?foo=bar&baz=qux&keep=this"; + const expected = "https://example.com/page?keep=this"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo", "baz"] + }); + expect(result, "should remove only specified params").to.equal(expected); + }); + + it("should handle single parameter stripping", function () { + const url = "https://example.com/page?remove=this&keep=that"; + const expected = "https://example.com/page?keep=that"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["remove"] + }); + expect(result, "should remove single specified param").to.equal(expected); + }); + + it("should return URL without query string if all params are stripped", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo", "baz"] + }); + expect(result, "should remove query string when all params stripped").to.equal(expected); + }); + + it("should handle case where specified params do not exist", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["nonexistent", "alsonothere"] + }); + expect(result, "should handle non-existent params gracefully").to.equal(url); + }); + + it("should handle empty param list", function () { + const url = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripQueryParams: [] }); + expect(result, "should not strip params with empty list").to.equal(url); + }); + + it("should preserve param order for remaining params", function () { + const url = "https://example.com/page?a=1&b=2&c=3&d=4"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["b", "d"] + }); + expect(result, "should preserve order of remaining params").to.include("a=1"); + expect(result, "should preserve order of remaining params").to.include("c=3"); + expect(result, "should not include stripped param b").to.not.include("b=2"); + expect(result, "should not include stripped param d").to.not.include("d=4"); + }); + }); + + describe("error handling", function () { + it("should return null or undefined input unchanged", function () { + expect(neuwo.cleanUrl(null, {}), "should handle null input").to.equal(null); + expect(neuwo.cleanUrl(undefined, {}), "should handle undefined input").to.equal(undefined); + expect(neuwo.cleanUrl("", {}), "should handle empty string").to.equal(""); + }); + + it("should return invalid URL unchanged and log error", function () { + const invalidUrl = "not-a-valid-url"; + const result = neuwo.cleanUrl(invalidUrl, { stripAllQueryParams: true }); + expect(result, "should return invalid URL unchanged").to.equal(invalidUrl); + }); + + it("should handle malformed URLs gracefully", function () { + const malformedUrl = "http://"; + const result = neuwo.cleanUrl(malformedUrl, { stripAllQueryParams: true }); + expect(result, "should return malformed URL unchanged").to.equal(malformedUrl); + }); + }); + + describe("when stripFragments is enabled", function () { + it("should strip URL fragments from URLs without query params", function () { + const url = "https://example.com/page#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove hash fragment").to.equal(expected); + }); + + it("should strip URL fragments from URLs with query params", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove hash fragment and preserve query params").to.equal(expected); + }); + + it("should strip fragments when combined with stripAllQueryParams", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripAllQueryParams: true, stripFragments: true }); + expect(result, "should remove both query params and fragment").to.equal(expected); + }); + + it("should strip fragments when combined with stripQueryParamsForDomains", function () { + const url = "https://example.com/page?foo=bar#section"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripFragments: true + }); + expect(result, "should remove both query params and fragment for matching domain").to.equal(expected); + }); + + it("should strip fragments when combined with stripQueryParams", function () { + const url = "https://example.com/page?foo=bar&keep=this#section"; + const expected = "https://example.com/page?keep=this"; + const result = neuwo.cleanUrl(url, { + stripQueryParams: ["foo"], + stripFragments: true + }); + expect(result, "should remove specified query params and fragment").to.equal(expected); + }); + + it("should handle URLs without fragments gracefully", function () { + const url = "https://example.com/page?foo=bar"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should handle URLs without fragments").to.equal(expected); + }); + + it("should handle empty fragments", function () { + const url = "https://example.com/page#"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove empty fragment").to.equal(expected); + }); + + it("should handle complex fragments with special characters", function () { + const url = "https://example.com/page?foo=bar#section-1/subsection?query"; + const expected = "https://example.com/page?foo=bar"; + const result = neuwo.cleanUrl(url, { stripFragments: true }); + expect(result, "should remove complex fragments").to.equal(expected); + }); + }); + + describe("option priority", function () { + it("should apply stripAllQueryParams first when multiple options are set", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripAllQueryParams: true, + stripQueryParams: ["foo"] + }); + expect(result, "stripAllQueryParams should take precedence").to.equal(expected); + }); + + it("should apply stripQueryParamsForDomains before stripQueryParams", function () { + const url = "https://example.com/page?foo=bar&baz=qux"; + const expected = "https://example.com/page"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripQueryParams: ["foo"] + }); + expect(result, "domain-specific stripping should take precedence").to.equal(expected); + }); + + it("should not strip for non-matching domain even with stripQueryParams set", function () { + const url = "https://other.com/page?foo=bar&baz=qux"; + const expected = "https://other.com/page?baz=qux"; + const result = neuwo.cleanUrl(url, { + stripQueryParamsForDomains: ["example.com"], + stripQueryParams: ["foo"] + }); + expect(result, "should fall through to stripQueryParams for non-matching domain").to.equal(expected); + }); + }); + }); + + // Integration Tests + describe("injectIabCategories edge cases and merging", function () { + it("should not inject data if 'marketing_categories' is missing from the successful API response", function () { + const apiResponse = { brand_safety: { BS_score: "1.0" } }; // Missing marketing_categories + const bidsConfig = bidsConfiglike(); + const bidsConfigCopy = bidsConfiglike(); + const conf = config(); + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // After a successful response with missing data, the global ortb2 fragments should remain empty + // as the data injection logic checks for marketingCategories. + expect( + bidsConfig.ortb2Fragments.global, + "The global ORTB fragments should remain empty" + ).to.deep.equal(bidsConfigCopy.ortb2Fragments.global); + }); + + it("should append content and user data to existing ORTB fragments", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig = bidsConfiglike(); + // Simulate existing first-party data from another source/module + const existingContentData = { name: "other_content_provider", segment: [{ id: "1" }] }; + const existingUserData = { name: "other_user_provider", segment: [{ id: "2" }] }; + + bidsConfig.ortb2Fragments.global = { + site: { + content: { + data: [existingContentData], + }, + }, + user: { + data: [existingUserData], + }, + }; + const conf = config(); + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + request.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + const siteData = bidsConfig.ortb2Fragments.global.site.content.data; + const userData = bidsConfig.ortb2Fragments.global.user.data; + + // Check that the existing data is still there (index 0) + expect(siteData[0], "Existing site.content.data should be preserved").to.deep.equal( + existingContentData + ); + expect(userData[0], "Existing user.data should be preserved").to.deep.equal(existingUserData); + + // Check that the new Neuwo data is appended (index 1) + expect(siteData.length, "site.content.data array should have 2 entries").to.equal(2); + expect(userData.length, "user.data array should have 2 entries").to.equal(2); + expect(siteData[1].name, "The appended content data should be from Neuwo").to.equal( + neuwo.DATA_PROVIDER + ); + expect(userData[1].name, "The appended user data should be from Neuwo").to.equal( + neuwo.DATA_PROVIDER + ); + }); + }); + + describe("getBidRequestData with caching", function () { + describe("when enableCache is true (default)", function () { + it("should cache the API response and reuse it on subsequent calls", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=1"; + + // First call should make an API request + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should use cached response (no new API request) + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should not make a new API request").to.equal(1); + + // Both configs should have the same data + const contentData1 = bidsConfig1.ortb2Fragments.global.site.content.data[0]; + const contentData2 = bidsConfig2.ortb2Fragments.global.site.content.data[0]; + expect(contentData1, "First config should have Neuwo data").to.exist; + expect(contentData2, "Second config should have Neuwo data from cache").to.exist; + expect(contentData1.name, "First config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + expect(contentData2.name, "Second config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + }); + + it("should cache when enableCache is explicitly set to true", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=2"; + conf.params.enableCache = true; + + // First call + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should use cache + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should use cached response").to.equal(1); + }); + }); + + describe("when enableCache is false", function () { + it("should not cache the API response and make a new request each time", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=3"; + conf.params.enableCache = false; + + // First call should make an API request + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call should make a new API request (not use cache) + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should make a new API request").to.equal(2); + + const request2 = server.requests[1]; + request2.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Both configs should have the same data structure + const contentData1 = bidsConfig1.ortb2Fragments.global.site.content.data[0]; + const contentData2 = bidsConfig2.ortb2Fragments.global.site.content.data[0]; + expect(contentData1, "First config should have Neuwo data").to.exist; + expect(contentData2, "Second config should have Neuwo data from new request").to.exist; + expect(contentData1.name, "First config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + expect(contentData2.name, "Second config should have correct provider").to.equal(neuwo.DATA_PROVIDER); + }); + + it("should bypass existing cache when enableCache is false", function () { + const apiResponse = getNeuwoApiResponse(); + const bidsConfig1 = bidsConfiglike(); + const bidsConfig2 = bidsConfiglike(); + const bidsConfig3 = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?id=4"; + + // First call with caching enabled (default) + neuwo.getBidRequestData(bidsConfig1, () => {}, conf, "consent data"); + expect(server.requests.length, "First call should make an API request").to.equal(1); + + const request1 = server.requests[0]; + request1.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + + // Second call with caching enabled should use cache + neuwo.getBidRequestData(bidsConfig2, () => {}, conf, "consent data"); + expect(server.requests.length, "Second call should use cache").to.equal(1); + + // Third call with caching disabled should bypass cache + conf.params.enableCache = false; + neuwo.getBidRequestData(bidsConfig3, () => {}, conf, "consent data"); + expect(server.requests.length, "Third call should bypass cache and make new request").to.equal(2); + + const request2 = server.requests[1]; + request2.respond( + 200, + { "Content-Type": "application/json; encoding=UTF-8" }, + JSON.stringify(apiResponse) + ); + }); + }); + }); + + describe("getBidRequestData with URL query param stripping", function () { + describe("when stripAllQueryParams is enabled", function () { + it("should strip all query parameters from the analyzed URL", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test&utm_campaign=example&id=5"; + conf.params.stripAllQueryParams = true; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should not contain encoded query params").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + }); + }); + + describe("when stripQueryParamsForDomains is enabled", function () { + it("should strip query params only for matching domains", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?foo=bar&id=5"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the URL without query params").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain the id param").to.not.include( + encodeURIComponent("id=5") + ); + }); + + it("should not strip query params for non-matching domains", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://other-domain.com/page?foo=bar&id=5"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the full URL with query params").to.include( + encodeURIComponent("https://other-domain.com/page?foo=bar&id=5") + ); + }); + + it("should handle subdomain matching correctly", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://sub.publisher.works/page?tracking=123"; + conf.params.stripQueryParamsForDomains = ["publisher.works"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should strip params for subdomain").to.include( + encodeURIComponent("https://sub.publisher.works/page") + ); + expect(request.url, "The request URL should not contain tracking param").to.not.include( + encodeURIComponent("tracking=123") + ); + }); + }); + + describe("when stripQueryParams is enabled", function () { + it("should strip only specified query parameters", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test&utm_campaign=example&id=5"; + conf.params.stripQueryParams = ["utm_source", "utm_campaign"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the id param").to.include( + encodeURIComponent("id=5") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + expect(request.url, "The request URL should not contain utm_campaign").to.not.include( + encodeURIComponent("utm_campaign") + ); + }); + + it("should handle stripping params that result in no query string", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + conf.params.websiteToAnalyseUrl = "https://publisher.works/article.php?utm_source=test"; + conf.params.stripQueryParams = ["utm_source"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should not contain a query string").to.include( + encodeURIComponent("https://publisher.works/article.php") + ); + expect(request.url, "The request URL should not contain utm_source").to.not.include( + encodeURIComponent("utm_source") + ); + }); + + it("should leave URL unchanged if specified params do not exist", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + const originalUrl = "https://publisher.works/article.php?id=5"; + conf.params.websiteToAnalyseUrl = originalUrl; + conf.params.stripQueryParams = ["utm_source", "nonexistent"]; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the original URL").to.include( + encodeURIComponent(originalUrl) + ); + }); + }); + + describe("when no stripping options are provided", function () { + it("should send the URL with all query parameters intact", function () { + const bidsConfig = bidsConfiglike(); + const conf = config(); + const originalUrl = "https://publisher.works/article.php?get=horrible_url_for_testing&id=5"; + conf.params.websiteToAnalyseUrl = originalUrl; + + neuwo.getBidRequestData(bidsConfig, () => {}, conf, "consent data"); + const request = server.requests[0]; + + expect(request.url, "The request URL should contain the full original URL").to.include( + encodeURIComponent(originalUrl) + ); + }); + }); + }); +}); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7756e96bd99..d3ba872946f 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { - spec, STORAGE, getNexx360LocalStorage, + spec, STORAGE, getNexx360LocalStorage, getGzipSetting, } from 'modules/nexx360BidAdapter.js'; import sinon from 'sinon'; import { getAmxId } from '../../../libraries/nexx360Utils/index.js'; @@ -33,6 +33,11 @@ describe('Nexx360 bid adapter tests', () => { }, }; + it('We test getGzipSettings', () => { + const output = getGzipSetting(); + expect(output).to.be.a('boolean'); + }); + describe('isBidRequestValid()', () => { let bannerBid; beforeEach(() => { @@ -95,12 +100,12 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() }); - }) + }); describe('getNexx360LocalStorage enabled but nothing', () => { before(() => { @@ -115,7 +120,7 @@ describe('Nexx360 bid adapter tests', () => { after(() => { sandbox.restore() }); - }) + }); describe('getNexx360LocalStorage enabled but wrong payload', () => { before(() => { @@ -125,7 +130,7 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the nexx360Id', () => { const output = getNexx360LocalStorage(); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -155,7 +160,7 @@ describe('Nexx360 bid adapter tests', () => { }); it('We test if we get the amxId', () => { const output = getAmxId(STORAGE, 'nexx360'); - expect(output).to.be.eql(false); + expect(output).to.be.eql(null); }); after(() => { sandbox.restore() @@ -303,6 +308,9 @@ describe('Nexx360 bid adapter tests', () => { }, nexx360: { tagId: 'luvxjvgn', + adUnitName: 'header-ad', + adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', + divId: 'div-1', }, adUnitName: 'header-ad', adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', @@ -324,6 +332,7 @@ describe('Nexx360 bid adapter tests', () => { divId: 'div-2-abcd', nexx360: { placement: 'testPlacement', + divId: 'div-2-abcd', allBids: true, }, }, @@ -335,8 +344,10 @@ describe('Nexx360 bid adapter tests', () => { version: requestContent.ext.version, source: 'prebid.js', pageViewId: requestContent.ext.pageViewId, - bidderVersion: '6.3', - localStorage: { amxId: 'abcdef'} + bidderVersion: '7.1', + localStorage: { amxId: 'abcdef'}, + sessionId: requestContent.ext.sessionId, + requestCounter: 0, }, cur: [ 'USD', @@ -564,6 +575,7 @@ describe('Nexx360 bid adapter tests', () => { mediaType: 'outstream', ssp: 'appnexus', adUnitCode: 'div-1', + divId: 'div-1', }, }, ], @@ -585,6 +597,7 @@ describe('Nexx360 bid adapter tests', () => { creativeId: '97517771', currency: 'USD', netRevenue: true, + divId: 'div-1', ttl: 120, mediaType: 'video', meta: { advertiserDomains: ['appnexus.com'], demandSource: 'appnexus' }, diff --git a/test/spec/modules/nodalsAiRtdProvider_spec.js b/test/spec/modules/nodalsAiRtdProvider_spec.js index f07d13fd11b..486822f3934 100644 --- a/test/spec/modules/nodalsAiRtdProvider_spec.js +++ b/test/spec/modules/nodalsAiRtdProvider_spec.js @@ -147,6 +147,7 @@ describe('NodalsAI RTD Provider', () => { const noPurpose1UserConsent = generateGdprConsent({ purpose1Consent: false }); const noPurpose7UserConsent = generateGdprConsent({ purpose7Consent: false }); const outsideGdprUserConsent = generateGdprConsent({ gdprApplies: false }); + const leastPermissiveUserConsent = generateGdprConsent({ purpose1Consent: false, purpose7Consent: false, nodalsConsent: false }); beforeEach(() => { sandbox = sinon.createSandbox(); @@ -263,6 +264,15 @@ describe('NodalsAI RTD Provider', () => { expect(result).to.be.true; expect(server.requests.length).to.equal(1); }); + + it('should return true with publisherProvidedConsent flag set and least permissive consent', function () { + const configWithManagedConsent = { params: { propertyId: '10312dd2', publisherProvidedConsent: true } }; + const result = nodalsAiRtdSubmodule.init(configWithManagedConsent, leastPermissiveUserConsent); + server.respond(); + + expect(result).to.be.true; + expect(server.requests.length).to.equal(1); + }); }); describe('when initialised with valid config and data already in storage', () => { @@ -617,6 +627,24 @@ describe('NodalsAI RTD Provider', () => { expect(result).to.deep.equal({}); }); + + it('should return targeting data with publisherProvidedConsent flag set and least permissive consent', () => { + createTargetingEngineStub(engineGetTargetingDataReturnValue); + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const configWithManagedConsent = { params: { propertyId: '10312dd2', publisherProvidedConsent: true } }; + + const result = nodalsAiRtdSubmodule.getTargetingData( + ['adUnit1'], + configWithManagedConsent, + leastPermissiveUserConsent + ); + server.respond(); + + expect(result).to.deep.equal(engineGetTargetingDataReturnValue); + }); }); describe('getBidRequestData()', () => { @@ -703,6 +731,33 @@ describe('NodalsAI RTD Provider', () => { expect(args[3].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); expect(server.requests.length).to.equal(0); }); + + it('should proxy the correct data to engine.getBidRequestData with publisherProvidedConsent flag set and least permissive consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const callback = sinon.spy(); + const reqBidsConfigObj = {dummy: 'obj'} + const configWithManagedConsent = { params: { propertyId: '10312dd2', publisherProvidedConsent: true } }; + nodalsAiRtdSubmodule.getBidRequestData( + reqBidsConfigObj, callback, configWithManagedConsent, leastPermissiveUserConsent + ); + server.respond(); + + expect(callback.called).to.be.false; + expect(engine.init.called).to.be.true; + expect(engine.getBidRequestData.called).to.be.true; + const args = engine.getBidRequestData.getCall(0).args; + expect(args[0]).to.deep.equal(reqBidsConfigObj); + expect(args[1]).to.deep.equal(callback); + expect(args[2]).to.deep.equal(leastPermissiveUserConsent); + expect(args[3].deps).to.deep.equal(successPubEndpointResponse.deps); + expect(args[3].facts).to.deep.include(successPubEndpointResponse.facts); + expect(args[3].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); }); describe('onBidResponseEvent()', () => { @@ -782,6 +837,30 @@ describe('NodalsAI RTD Provider', () => { expect(args[2].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); expect(server.requests.length).to.equal(0); }); + + it('should proxy the correct data to engine.onBidResponseEvent with publisherProvidedConsent flag set and least permissive consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const bidResponse = {dummy: 'obj', 'bid': 'foo'}; + const configWithManagedConsent = { params: { propertyId: '10312dd2', publisherProvidedConsent: true } }; + nodalsAiRtdSubmodule.onBidResponseEvent( + bidResponse, configWithManagedConsent, leastPermissiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.true; + expect(engine.onBidResponseEvent.called).to.be.true; + const args = engine.onBidResponseEvent.getCall(0).args; + expect(args[0]).to.deep.equal(bidResponse); + expect(args[1]).to.deep.equal(leastPermissiveUserConsent); + expect(args[2].deps).to.deep.equal(successPubEndpointResponse.deps); + expect(args[2].facts).to.deep.include(successPubEndpointResponse.facts); + expect(args[2].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); }); describe('onAuctionEndEvent()', () => { @@ -861,5 +940,29 @@ describe('NodalsAI RTD Provider', () => { expect(args[2].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); expect(server.requests.length).to.equal(0); }); + + it('should proxy the correct data to engine.onAuctionEndEvent with publisherProvidedConsent flag set and least permissive consent', () => { + setDataInLocalStorage({ + data: successPubEndpointResponse, + createdAt: Date.now(), + }); + const engine = createTargetingEngineStub(); + const auctionDetails = {dummy: 'obj', auction: 'foo'}; + const configWithManagedConsent = { params: { propertyId: '10312dd2', publisherProvidedConsent: true } }; + nodalsAiRtdSubmodule.onAuctionEndEvent( + auctionDetails, configWithManagedConsent, leastPermissiveUserConsent + ); + server.respond(); + + expect(engine.init.called).to.be.true; + expect(engine.onAuctionEndEvent.called).to.be.true; + const args = engine.onAuctionEndEvent.getCall(0).args; + expect(args[0]).to.deep.equal(auctionDetails); + expect(args[1]).to.deep.equal(leastPermissiveUserConsent); + expect(args[2].deps).to.deep.equal(successPubEndpointResponse.deps); + expect(args[2].facts).to.deep.include(successPubEndpointResponse.facts); + expect(args[2].campaigns).to.deep.equal(successPubEndpointResponse.campaigns); + expect(server.requests.length).to.equal(0); + }); }); }); diff --git a/test/spec/modules/nubaBidAdapter_spec.js b/test/spec/modules/nubaBidAdapter_spec.js new file mode 100644 index 00000000000..ad08015455e --- /dev/null +++ b/test/spec/modules/nubaBidAdapter_spec.js @@ -0,0 +1,475 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/nubaBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'nuba'; + +describe('NubaBidAdapter', function () { + const userIdAsEids = [{ + source: 'test.org', + uids: [{ + id: '01**********', + atype: 1, + ext: { + third: '01***********' + } + }] + }]; + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + }, + userIdAsEids + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative' + }, + userIdAsEids + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, + refererInfo: { + referer: 'https://test.com', + page: 'https://test.com' + }, + ortb2: { + device: { + w: 1512, + h: 982, + language: 'en-UK', + } + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns general data valid', function () { + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys( + 'deviceWidth', + 'deviceHeight', + 'device', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax', + 'bcat', + 'badv', + 'bapp', + 'battr' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('object'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns valid endpoints', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + endpointId: 'testBanner', + }, + userIdAsEids + } + ]; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.endpointId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('network'); + expect(placement.eids).to.exist.and.to.be.deep.equal(userIdAsEids); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2 = bidderRequest.ortb2 || {}; + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + const serverRequest = spec.buildRequests(bids, bidderRequest); + const data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + const dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + const dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + const nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + const dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + const serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + const serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + const serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + const serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/opscoBidAdapter_spec.js b/test/spec/modules/opscoBidAdapter_spec.js index 77051f56de1..c0095edb0f1 100644 --- a/test/spec/modules/opscoBidAdapter_spec.js +++ b/test/spec/modules/opscoBidAdapter_spec.js @@ -1,6 +1,8 @@ import {expect} from 'chai'; import {spec} from 'modules/opscoBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import {addFPDToBidderRequest} from "../../helpers/fpd.js"; +import 'modules/priceFloors.js'; describe('opscoBidAdapter', function () { const adapter = newBidder(spec); @@ -69,6 +71,7 @@ describe('opscoBidAdapter', function () { beforeEach(function () { validBid = { + bidId: 'bid123', bidder: 'opsco', params: { placementId: '123', @@ -82,16 +85,12 @@ describe('opscoBidAdapter', function () { }; bidderRequest = { - bidderRequestId: 'bid123', + bidderRequestId: 'bidderRequestId', refererInfo: { domain: 'example.com', page: 'https://example.com/page', ref: 'https://referrer.com' - }, - gdprConsent: { - consentString: 'GDPR_CONSENT_STRING', - gdprApplies: true - }, + } }; }); @@ -113,39 +112,60 @@ describe('opscoBidAdapter', function () { }); }); - it('should send GDPR consent in the payload if present', function () { - const request = spec.buildRequests([validBid], bidderRequest); - expect(JSON.parse(request.data).user.ext.consent).to.deep.equal('GDPR_CONSENT_STRING'); + it('should send GDPR consent in the payload if present', async function () { + const request = spec.buildRequests([validBid], await addFPDToBidderRequest({ + ...bidderRequest, + gdprConsent: { + consentString: 'GDPR_CONSENT_STRING', + gdprApplies: true + } + })); + expect(request.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(request.data.user.ext.consent).to.deep.equal('GDPR_CONSENT_STRING'); }); - it('should send CCPA in the payload if present', function () { - const ccpa = '1YYY'; - bidderRequest.uspConsent = ccpa; - const request = spec.buildRequests([validBid], bidderRequest); - expect(JSON.parse(request.data).regs.ext.us_privacy).to.equal(ccpa); + it('should send CCPA in the payload if present', async function () { + const request = spec.buildRequests([validBid], await addFPDToBidderRequest({ + ...bidderRequest, + ...{uspConsent: '1YYY'} + })); + expect(request.data.regs.ext.us_privacy).to.equal('1YYY'); }); - it('should send eids in the payload if present', function () { - const eids = {data: [{source: 'test', uids: [{id: '123', ext: {}}]}]}; - validBid.userIdAsEids = eids; - const request = spec.buildRequests([validBid], bidderRequest); - expect(JSON.parse(request.data).user.ext.eids).to.deep.equal(eids); + it('should send eids in the payload if present', async function () { + const eids = [{source: 'test', uids: [{id: '123', ext: {}}]}]; + const request = spec.buildRequests([validBid], await addFPDToBidderRequest({ + ...bidderRequest, + ortb2: {user: {ext: {eids: eids}}} + })); + expect(request.data.user.ext.eids).to.deep.equal(eids); }); it('should send schain in the payload if present', function () { const schain = {'ver': '1.0', 'complete': 1, 'nodes': [{'asi': 'exchange1.com', 'sid': '1234', 'hp': 1}]}; - validBid.ortb2 = validBid.ortb2 || {}; - validBid.ortb2.source = validBid.ortb2.source || {}; - validBid.ortb2.source.ext = validBid.ortb2.source.ext || {}; - validBid.ortb2.source.ext.schain = schain; + const request = spec.buildRequests([validBid], { + ...bidderRequest, + ortb2: {source: {ext: {schain: schain}}} + }); + expect(request.data.source.ext.schain).to.deep.equal(schain); + }); + + it('should send bid floor', function () { const request = spec.buildRequests([validBid], bidderRequest); - expect(JSON.parse(request.data).source.ext.schain).to.deep.equal(schain); + expect(request.data.imp[0].id).to.equal('bid123'); + expect(request.data.imp[0].bidfloorcur).to.not.exist; + + const getFloorResponse = { currency: 'USD', floor: 3 }; + validBid.getFloor = () => getFloorResponse; + const requestWithFloor = spec.buildRequests([validBid], bidderRequest); + expect(requestWithFloor.data.imp[0].bidfloor).to.equal(3); + expect(requestWithFloor.data.imp[0].bidfloorcur).to.equal('USD'); }); it('should correctly identify test mode', function () { validBid.params.test = true; const request = spec.buildRequests([validBid], bidderRequest); - expect(JSON.parse(request.data).test).to.equal(1); + expect(request.data.test).to.equal(1); }); }); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index adcc84dcf73..d3165bd3c7d 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -37,6 +37,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); @@ -54,6 +55,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v0'); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); @@ -72,6 +74,7 @@ describe('Optimera RTD score file URL is properly set for v0', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost%3A9876/context.html.js'); }); @@ -91,6 +94,7 @@ describe('Optimera RTD score file URL is properly set for v1', () => { }] }; optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScoresURL(); optimeraRTD.setScores(); expect(optimeraRTD.apiVersion).to.equal('v1'); expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 4173cd88b1b..595b95c6db8 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -3,12 +3,11 @@ import { PrebidServer as Adapter, resetSyncedStatus, validateConfig, - s2sDefaultConfig, - processPBSRequest + s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; +import {deepAccess, deepClone, getWinDimensions, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; import {config} from 'src/config.js'; import * as events from 'src/events.js'; @@ -626,6 +625,7 @@ describe('S2S Adapter', function () { 'auctionId': '173afb6d132ba3', 'bidderRequestId': '3d1063078dfcc8', 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'pageViewId': '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', 'bids': [ { 'bidder': 'appnexus', @@ -1195,8 +1195,8 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', @@ -1227,8 +1227,8 @@ describe('S2S Adapter', function () { const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) sinon.assert.match(requestBid.app, { bundle: 'com.test.app', @@ -1619,8 +1619,8 @@ describe('S2S Adapter', function () { adapter.callBids(await addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { - w: window.screen.width, - h: window.screen.height, + w: getWinDimensions().screen.width, + h: getWinDimensions().screen.height, }) expect(requestBid.imp[0].native.ver).to.equal('1.2'); }); @@ -2384,7 +2384,7 @@ describe('S2S Adapter', function () { expect(requestBid.ext.prebid.targeting.includewinners).to.equal(true); }); - it('adds s2sConfig video.ext.prebid to request for ORTB', function () { + it('adds custom property in s2sConfig.extPrebid to request for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { foo: 'bar' @@ -2415,7 +2415,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { + it('overrides request.ext.prebid properties using s2sConfig.extPrebid values for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { targeting: { @@ -2448,7 +2448,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.prebid properties using s2sConfig video.ext.prebid values for ORTB', function () { + it('overrides request.ext.prebid properties and adds custom property from s2sConfig.extPrebid for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { cache: { @@ -2703,6 +2703,21 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.ext.prebid.multibid).to.deep.equal(expected); }); + it('passes page view IDs per bidder in request', function () { + const clonedBidRequest = utils.deepClone(BID_REQUESTS[0]); + clonedBidRequest.bidderCode = 'some-other-bidder'; + clonedBidRequest.pageViewId = '490a1cbc-a03c-429a-b212-ba3649ca820c'; + const bidRequests = [BID_REQUESTS[0], clonedBidRequest]; + const expected = { + appnexus: '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', + 'some-other-bidder': '490a1cbc-a03c-429a-b212-ba3649ca820c' + }; + + adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.prebid.page_view_ids).to.deep.equal(expected); + }); + it('sets and passes pbjs version in request if channel does not exist in s2sConfig', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); diff --git a/test/spec/modules/programmaticXBidAdapter_spec.js b/test/spec/modules/programmaticXBidAdapter_spec.js index ef7f0caf797..2c857efc8fb 100644 --- a/test/spec/modules/programmaticXBidAdapter_spec.js +++ b/test/spec/modules/programmaticXBidAdapter_spec.js @@ -242,6 +242,10 @@ describe('programmaticXBidAdapter', function () { expect(adapter.code).to.exist.and.to.be.a('string'); }); + it('exists and is a number', function () { + expect(adapter.gvlid).to.exist.and.to.be.a('number'); + }) + it('exists and contains media types', function () { expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); diff --git a/test/spec/modules/publicGoodBidAdapter_spec.js b/test/spec/modules/publicGoodBidAdapter_spec.js new file mode 100644 index 00000000000..87490ff2086 --- /dev/null +++ b/test/spec/modules/publicGoodBidAdapter_spec.js @@ -0,0 +1,188 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { spec, storage } from 'modules/publicGoodBidAdapter.js'; +import { hook } from 'src/hook.js'; + +describe('Public Good Adapter', function () { + let validBidRequests; + + beforeEach(function () { + validBidRequests = [ + { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + }, + placementCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [], + }, + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + }, + ]; + }); + + describe('for requests', function () { + describe('without partner ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'publicgood', + params: { + slotId: 'all', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('without slot ID', function () { + it('rejects the bid', function () { + const invalidBid = { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + }, + }; + const isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + + describe('with a valid bid', function () { + it('accepts the bid', function () { + const validBid = { + bidder: 'publicgood', + params: { + partnerId: 'prebid-test', + slotId: 'test' + }, + }; + const isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + }); + }); + + describe('for server responses', function () { + let serverResponse; + + describe('with no body', function () { + beforeEach(function() { + serverResponse = { + body: null, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with action=hide', function () { + beforeEach(function() { + serverResponse = { + body: { + action: 'Hide', + }, + }; + }); + + it('does not return any bids', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(0); + }); + }); + + describe('with a valid campaign', function () { + beforeEach(function() { + serverResponse = { + body: { + "targetData": { + "deviceType": "desktop", + "parent_org": "prebid-test", + "cpm": 3, + "target_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "deviceInfo": { + "os": { + "name": "Mac OS", + "version": "10.15.7" + }, + "engine": { + "name": "Blink", + "version": "130.0.0.0" + }, + "browser": { + "major": "130", + "name": "Chrome", + "version": "130.0.0.0" + }, + "cpu": {}, + "ua": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/130.0.0.0 Safari\/537.36", + "device": { + "vendor": "Apple", + "model": "Macintosh" + } + }, + "widget_type": "card", + "isInApp": false, + "partner_id": "prebid-test", + "countryCode": "US", + "metroCode": "602", + "hasReadMore": false, + "region": "IL", + "campaign_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61" + }, + "action": "Default", + "url": "https%3A%2F%2Fpublicgood.com%2F", + "content": { + "parent_org": "prebid-test", + "rules_match_info": null, + "content_id": 20446189, + "all_matches": [ + { + "analysis_tag": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "guid": "a9b430ab-1f62-46f3-9d3a-1ece821dca61" + } + ], + "is_override": true, + "cid_match_type": "", + "target_id": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "url_id": 128113623, + "title": "Public Good", + "hide": false, + "partner_id": "prebid-test", + "qa_verified": true, + "tag": "a9b430ab-1f62-46f3-9d3a-1ece821dca61", + "is_filter": false + } + } + }; + }); + + it('returns a complete bid', function () { + const bids = spec.interpretResponse(serverResponse, { bidRequest: validBidRequests[0] }); + + expect(bids).to.be.length(1); + expect(bids[0].cpm).to.equal(3); + expect(bids[0].width).to.equal(320); + expect(bids[0].height).to.equal(470); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.have.string('data-pgs-partner-id="prebid-test"'); + }); + }); + }); +}); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index fd75c6d0c4d..5f20024fec2 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -87,7 +87,34 @@ describe('Richaudience adapter tests', function () { bidId: '2c7c8e9c900244', ortb2Imp: { ext: { - gpid: '/19968336/header-bid-tag-1#example-2', + gpid: '/19968336/header-bid-tag-1#example-2' + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], [300, 600], [728, 90], [970, 250]] + } + }, + bidder: 'richaudience', + params: { + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site', + keywords: 'key1=value1;key2=value2' + }, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }]; + + var DEFAULT_PARAMS_NEW_SIZES_PBADSLOT = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + ortb2Imp: { + ext: { data: { pbadslot: '/19968336/header-bid-tag-1#example-2' } @@ -863,7 +890,7 @@ describe('Richaudience adapter tests', function () { expect(requestContent.dsa.transparency[0]).to.have.property('domain').and.to.equal('richaudience.com'); }) - it('should pass gpid', function () { + it('should pass gpid with gpid', function () { const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES_GPID, { gdprConsent: { consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', @@ -874,6 +901,17 @@ describe('Richaudience adapter tests', function () { const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('gpid').and.to.equal('/19968336/header-bid-tag-1#example-2'); }) + it('should pass gpid with pbadslot', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_NEW_SIZES_PBADSLOT, { + gdprConsent: { + consentString: 'BOZcQl_ObPFjWAeABAESCD-AAAAjx7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__59__3z3_NohBgA', + gdprApplies: true + }, + refererInfo: {} + }) + const requestContent = JSON.parse(request[0].data); + expect(requestContent).to.have.property('gpid').and.to.equal('/19968336/header-bid-tag-1#example-2'); + }) describe('onTimeout', function () { beforeEach(function () { diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 4f5de5a651e..bc7446a448c 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -543,6 +543,29 @@ describe('riseAdapter', function () { expect(request.data.bids[0].coppa).to.be.equal(1); }); }); + + describe('User Eids', function() { + it('should get the Eids from the userIdAsEids object and set them in the request', function() { + const bid = utils.deepClone(bidRequests[0]); + const userIds = [ + { + sourcer: 'pubcid.org', + uids: [{ + id: '12345678', + atype: 1, + }] + }]; + bid.userIdAsEids = userIds; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.params.userIds).to.be.equal(JSON.stringify(userIds)); + }); + + it('should not set the userIds request param if no userIdAsEids are set', function() { + const bid = utils.deepClone(bidRequests[0]); + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.params.userIds).to.be.undefined; + }); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 2f1fa127b28..e75190037bd 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -208,6 +208,128 @@ describe('RTBHouseAdapter', () => { expect(data.user.ext.consent).to.equal(''); }); + it('should populate GPP consent string when gppConsent.gppString is provided', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gppConsent: { + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', + applicableSections: [7] + } + }) + ); + const data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(data.regs.gpp_sid).to.deep.equal([7]); + }); + + it('should populate GPP consent with multiple applicable sections', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gppConsent: { + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', + applicableSections: [2, 6, 7] + } + }) + ); + const data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(data.regs.gpp_sid).to.deep.equal([2, 6, 7]); + }); + + it('should fallback to ortb2.regs.gpp when gppConsent.gppString is not provided', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const localBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', + gpp_sid: [8, 10] + } + } + }; + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(data.regs.gpp_sid).to.deep.equal([8, 10]); + }); + + it('should prioritize gppConsent.gppString over ortb2.regs.gpp', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const localBidderRequest = { + ...bidderRequest, + gppConsent: { + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', + applicableSections: [7] + }, + ortb2: { + regs: { + gpp: 'DIFFERENT_GPP_STRING', + gpp_sid: [8, 10] + } + } + }; + const request = spec.buildRequests(bidRequest, localBidderRequest); + const data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(data.regs.gpp_sid).to.deep.equal([7]); + }); + + it('should not populate GPP consent when neither gppConsent nor ortb2.regs.gpp is provided', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const request = spec.buildRequests(bidRequest, bidderRequest); + const data = JSON.parse(request.data); + expect(data).to.not.have.nested.property('regs.gpp'); + expect(data).to.not.have.nested.property('regs.gpp_sid'); + }); + + it('should not populate GPP when gppConsent exists but gppString is missing', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gppConsent: { + applicableSections: [7] + } + }) + ); + const data = JSON.parse(request.data); + expect(data).to.not.have.nested.property('regs.gpp'); + expect(data).to.not.have.nested.property('regs.gpp_sid'); + }); + + it('should handle both GDPR and GPP consent together', function () { + const bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' + }, + gppConsent: { + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', + applicableSections: [7] + } + }) + ); + const data = JSON.parse(request.data); + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.equal('BOJ8RZsOJ8RZsABAB8AAAAAZ-A'); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(data.regs.gpp_sid).to.deep.equal([7]); + }); + it('should include banner imp in request', () => { const bidRequest = Object.assign([], bidRequests); const request = spec.buildRequests(bidRequest, bidderRequest); @@ -222,17 +344,17 @@ describe('RTBHouseAdapter', () => { expect(data.source.tid).to.equal('bidderrequest-auction-id'); }); - it('should include bidfloor from floor module if avaiable', () => { + it('should include bidfloor from floor module if available', () => { const bidRequest = Object.assign([], bidRequests); - bidRequest[0].getFloor = () => ({floor: 1.22}); + bidRequest[0].getFloor = () => ({floor: 1.22, currency: 'USD'}); const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); expect(data.imp[0].bidfloor).to.equal(1.22) }); - it('should use bidfloor from floor module if both floor module and bid floor avaiable', () => { + it('should use bidfloor from floor module if both floor module and bid floor available', () => { const bidRequest = Object.assign([], bidRequests); - bidRequest[0].getFloor = () => ({floor: 1.22}); + bidRequest[0].getFloor = () => ({floor: 1.22, currency: 'USD'}); bidRequest[0].params.bidfloor = 0.01; const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index b96a5e4fd4f..a0a9c8ba194 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -2890,15 +2890,6 @@ describe('the rubicon adapter', function () { expect(slotParams.kw).to.equal('a,b,c'); }); - it('should pass along o_ae param when fledge is enabled', () => { - const localBidRequest = Object.assign({}, bidderRequest.bids[0]); - localBidRequest.ortb2Imp.ext.ae = true; - - const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); - - expect(slotParams['o_ae']).to.equal(1) - }); - it('should pass along desired segtaxes, but not non-desired ones', () => { const localBidderRequest = Object.assign({}, bidderRequest); localBidderRequest.refererInfo = {domain: 'bob'}; @@ -3816,43 +3807,6 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); - it('Should support recieving an auctionConfig and pass it along to Prebid', function () { - const response = { - 'status': 'ok', - 'account_id': 14062, - 'site_id': 70608, - 'zone_id': 530022, - 'size_id': 15, - 'alt_size_ids': [ - 43 - ], - 'tracking': '', - 'inventory': {}, - 'ads': [{ - 'status': 'ok', - 'cpm': 0, - 'size_id': 15 - }], - 'component_auction_config': [{ - 'random': 'value', - 'bidId': '5432' - }, - { - 'random': 'string', - 'bidId': '6789' - }] - }; - - const {bids, paapi} = spec.interpretResponse({body: response}, { - bidRequest: bidderRequest.bids[0] - }); - - expect(bids).to.be.lengthOf(1); - expect(paapi[0].bidId).to.equal('5432'); - expect(paapi[0].config.random).to.equal('value'); - expect(paapi[1].bidId).to.equal('6789'); - }); - it('should handle an error', function () { const response = { 'status': 'ok', diff --git a/test/spec/modules/scaliburBidAdapter_spec.js b/test/spec/modules/scaliburBidAdapter_spec.js new file mode 100644 index 00000000000..c2f6a3c65a8 --- /dev/null +++ b/test/spec/modules/scaliburBidAdapter_spec.js @@ -0,0 +1,340 @@ +import {expect} from 'chai'; +import {spec, getFirstPartyData, storage} from 'modules/scaliburBidAdapter.js'; + +describe('Scalibur Adapter', function () { + const BID = { + 'bidId': 'ec675add-d1d2-4bdd', + 'adUnitCode': '63540ad1df6f42d168cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'params': { + "placementId": "test-scl-placement", + "adUnitCode": "123", + "gpid": "/1234/5678/homepage", + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + "playerSize": [[300, 169]], + "mimes": [ + "video/mp4", + "application/javascript", + "video/webm" + ], + "protocols": [1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16], + "api": [1, 2, 7, 8, 9], + 'maxduration': 30, + 'minduration': 15, + 'startdelay': 0, + 'linearity': 1, + 'placement': 1, + "skip": 1, + "skipafter": 5, + }, + 'banner': { + 'sizes': [[300, 250], [728, 90]], + }, + }, + "ortb2Imp": { + "ext": { + "gpid": '/1234/5678/homepage', + }, + }, + }; + + const BIDDER_REQUEST = { + auctionId: 'auction-45678', + refererInfo: { + page: 'https://example-publisher.com', + domain: 'example-publisher.com', + }, + gdprConsent: { + gdprApplies: true, + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + }, + uspConsent: '1---', + ortb2: { + site: { + pagecat: ['IAB1-1', 'IAB3-2'], + ref: 'https://example-referrer.com', + }, + user: { + data: [{name: 'segments', segment: ['sports', 'entertainment']}], + }, + regs: { + ext: { + gpc: '1', + }, + }, + }, + timeout: 3000, + }; + + const DEFAULTS_BID = { + 'bidId': 'ec675add-f23d-4bdd', + 'adUnitCode': '63540ad1df6f42d168cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'params': { + "placementId": "test-scl-placement", + "adUnitCode": "123", + "gpid": "/1234/5678/homepage", + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'minduration': 15, + 'startdelay': 0, + }, + 'banner': { + 'sizes': [[300, 250], [728, 90]], + }, + }, + "ortb2Imp": { + "ext": { + "gpid": '/1234/5678/homepage', + }, + }, + }; + + const DEFAULTS_BIDDER_REQUEST = { + auctionId: 'auction-45633', + refererInfo: { + page: 'https://example-publisher.com', + domain: 'example-publisher.com', + }, + timeout: 3000, + }; + + describe('isBidRequestValid', function () { + it('should return true for valid bid params', function () { + expect(spec.isBidRequestValid(BID)).to.equal(true); + }); + + it('should return false for missing placementId', function () { + const invalidBid = {...BID, params: {}}; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should build a valid OpenRTB request', function () { + const request = spec.buildRequests([BID], BIDDER_REQUEST); + const payload = request.data; + + expect(payload.id).to.equal('auction-45678'); + expect(payload.imp).to.have.length(1); + + const imp = payload.imp[0]; + expect(imp.id).to.equal('ec675add-d1d2-4bdd'); + expect(imp.ext.placementId).to.equal('test-scl-placement'); + expect(imp.ext.gpid).to.equal('/1234/5678/homepage'); + expect(imp.banner.format).to.deep.equal([ + {w: 300, h: 250}, + {w: 728, h: 90}, + ]); + + const video = imp.video; + expect(video).to.exist; + expect(video.mimes).to.include('video/mp4'); + expect(video.w).to.equal(300); + expect(video.h).to.equal(169); + expect(video.placement).to.equal(1); + expect(video.plcmt).to.equal(1); + expect(video.api).to.include(7); + expect(payload.regs.ext.gpc).to.equal('1'); + }); + }); + + describe('buildRequests', function () { + it('should build a valid OpenRTB request with default values', function () { + const request = spec.buildRequests([DEFAULTS_BID], DEFAULTS_BIDDER_REQUEST); + const payload = request.data; + + expect(payload.id).to.equal('auction-45633'); + expect(payload.imp).to.have.length(1); + + const imp = payload.imp[0]; + expect(imp.id).to.equal('ec675add-f23d-4bdd'); + expect(imp.ext.placementId).to.equal('test-scl-placement'); + expect(imp.ext.gpid).to.equal('/1234/5678/homepage'); + expect(imp.banner.format).to.deep.equal([ + {w: 300, h: 250}, + {w: 728, h: 90}, + ]); + + const video = imp.video; + expect(video).to.exist; + expect(video.mimes).to.deep.equal(['video/mp4']); + expect(video.maxduration).to.equal(180); + expect(video.w).to.equal(640); + expect(video.h).to.equal(480); + expect(video.placement).to.equal(1); + expect(video.skip).to.equal(0); + expect(video.skipafter).to.equal(5); + expect(video.api).to.deep.equal([1, 2]); + expect(video.linearity).to.equal(1); + expect(payload.site.ref).to.equal(''); + expect(payload.site.pagecat).to.deep.equal([]); + expect(payload.user.consent).to.equal(''); + expect(payload.user.data).to.deep.equal([]); + expect(payload.regs.gdpr).to.equal(0); + expect(payload.regs.us_privacy).to.equal(''); + expect(payload.regs.ext.gpc).to.equal(''); + }); + }); + + describe('interpretResponse', function () { + it('should interpret server response correctly', function () { + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: '1', + cpm: 2.5, + width: 300, + height: 250, + crid: 'creative-23456', + adm: '
Sample Ad Markup
', + cur: 'USD', + }, + ], + }, + ], + }, + }; + const request = spec.buildRequests([BID], BIDDER_REQUEST); + const bidResponses = spec.interpretResponse(serverResponse, request); + + expect(bidResponses).to.have.length(1); + + const response = bidResponses[0]; + expect(response.requestId).to.equal('1'); + expect(response.cpm).to.equal(2.5); + expect(response.width).to.equal(300); + expect(response.height).to.equal(250); + expect(response.creativeId).to.equal('creative-23456'); + expect(response.currency).to.equal('USD'); + expect(response.netRevenue).to.equal(true); + expect(response.ttl).to.equal(300); + }); + }); + + describe('getUserSyncs', function () { + it('should return iframe and pixel sync URLs with correct params', function () { + const syncOptions = {iframeEnabled: true, pixelEnabled: true}; + const gdprConsent = { + gdprApplies: true, + consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', + }; + const uspConsent = '1---'; + + const syncs = spec.getUserSyncs(syncOptions, [], gdprConsent, uspConsent); + + expect(syncs).to.have.length(2); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA'); + expect(syncs[0].url).to.include('us_privacy=1---'); + expect(syncs[1].type).to.equal('image'); + }); + }); + + describe('getScaliburFirstPartyData', function () { + let sandbox; + let storageStub; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + storageStub = { + hasLocalStorage: sandbox.stub(), + getDataFromLocalStorage: sandbox.stub() + }; + + // Replace storage methods + sandbox.stub(storage, 'hasLocalStorage').callsFake(storageStub.hasLocalStorage); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake(storageStub.getDataFromLocalStorage); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should return undefined when localStorage is not available', function () { + storageStub.hasLocalStorage.returns(false); + + const result = getFirstPartyData(); + + expect(result).to.be.undefined; + expect(storageStub.getDataFromLocalStorage.called).to.be.false; + }); + + it('should return existing first party data when available', function () { + const existingData = { + pcid: 'existing-uuid-1234-5678-abcd-ef1234567890', + pcidDate: 1640995200000 + }; + + storageStub.hasLocalStorage.returns(true); + storageStub.getDataFromLocalStorage.returns(JSON.stringify(existingData)); + + const result = getFirstPartyData(); + + // Should use existing data + expect(result.pcid).to.equal(existingData.pcid); + expect(result.pcidDate).to.equal(existingData.pcidDate); + }); + }); + + describe('buildRequests with first party data', function () { + let sandbox; + let storageStub; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + storageStub = { + hasLocalStorage: sandbox.stub(), + getDataFromLocalStorage: sandbox.stub(), + }; + + sandbox.stub(storage, 'hasLocalStorage').callsFake(storageStub.hasLocalStorage); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake(storageStub.getDataFromLocalStorage); + }); + + afterEach(function () { + sandbox.restore(); + }); + + it('should include first party data in buildRequests when available', function () { + const testData = { + pcid: 'test-uuid-1234-5678-abcd-ef1234567890', + pcidDate: 1640995200000 + }; + + storageStub.hasLocalStorage.returns(true); + storageStub.getDataFromLocalStorage.returns(JSON.stringify(testData)); + + const request = spec.buildRequests([DEFAULTS_BID], DEFAULTS_BIDDER_REQUEST); + + expect(request.data.ext.pcid).to.equal(testData.pcid); + expect(request.data.ext.pcidDate).to.equal(testData.pcidDate); + }); + + it('should not include first party data when localStorage is unavailable', function () { + storageStub.hasLocalStorage.returns(false); + + const request = spec.buildRequests([DEFAULTS_BID], DEFAULTS_BIDDER_REQUEST); + + expect(request.data.ext.pcid).to.be.undefined; + expect(request.data.ext.pcidDate).to.be.undefined; + }); + }); +}); diff --git a/test/spec/modules/screencoreBidAdapter_spec.js b/test/spec/modules/screencoreBidAdapter_spec.js index 4e9177e8ce5..bd1aad95edf 100644 --- a/test/spec/modules/screencoreBidAdapter_spec.js +++ b/test/spec/modules/screencoreBidAdapter_spec.js @@ -1,789 +1,396 @@ import { expect } from 'chai'; -import { createDomain, spec as adapter, storage } from 'modules/screencoreBidAdapter.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { - extractCID, - extractPID, - extractSubDomain, - getStorageItem, - getUniqueDealId, - hashCode, - setStorageItem, - tryParseJSON, -} from 'libraries/vidazooUtils/bidderUtils.js'; +import { createDomain, spec as adapter } from 'modules/screencoreBidAdapter.js'; import { config } from 'src/config.js'; import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; -import { version } from 'package.json'; -import * as utils from 'src/utils.js'; -import sinon, { useFakeTimers } from 'sinon'; - -export const TEST_ID_SYSTEMS = ['criteoId', 'id5id', 'netId', 'tdid', 'pubProvidedId', 'intentIqId', 'liveIntentId']; - -const SUB_DOMAIN = 'exchange'; +import sinon from 'sinon'; const BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': 'div-gpt-ad-12345-0', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '59db6b3b4ffaa70004f45cdc', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1, - 'ext': { - 'param1': 'loremipsum', - 'param2': 'dolorsitamet' - }, - 'placementId': 'testBanner' + bidId: '2d52001cabd527', + bidder: 'screencore', + adUnitCode: 'div-gpt-ad-12345-0', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + params: { + placementId: 'testPlacement', + endpointId: 'testEndpoint' }, - 'placementCode': 'div-gpt-ad-1460505748561-0', - 'sizes': [[300, 250], [300, 600]], - 'bidderRequestId': '1fdb5ff1b6eaa7', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'mediaTypes': [BANNER], - 'ortb2Imp': { - 'ext': { - 'gpid': '0123456789', - 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + ortb2Imp: { + ext: { + gpid: '0123456789', + tid: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' } } }; const VIDEO_BID = { - 'bidId': '2d52001cabd527', - 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', - 'bidderRequestId': '12a8ae9ada9c13', - 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', - 'bidRequestsCount': 4, - 'bidderRequestsCount': 3, - 'bidderWinsCount': 1, - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', - 'params': { - 'subDomain': SUB_DOMAIN, - 'cId': '635509f7ff6642d368cb9837', - 'pId': '59ac17c192832d0011283fe3', - 'bidFloor': 0.1, - 'placementId': 'testBanner' + bidId: '2d52001cabd528', + bidder: 'screencore', + adUnitCode: 'video-ad-unit', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + params: { + placementId: 'testVideoPlacement', + endpointId: 'testVideoEndpoint' }, - 'sizes': [[545, 307]], - 'mediaTypes': { - 'video': { - 'playerSize': [[545, 307]], - 'context': 'instream', - 'mimes': [ - 'video/mp4', - 'application/javascript' - ], - 'protocols': [2, 3, 5, 6], - 'maxduration': 60, - 'minduration': 0, - 'startdelay': 0, - 'linearity': 1, - 'api': [2], - 'placement': 1 + mediaTypes: { + video: { + playerSize: [[545, 307]], + context: 'instream', + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 3, 5, 6], + maxduration: 60, + minduration: 0, + startdelay: 0, + linearity: 1, + api: [2], + placement: 1 } }, - 'ortb2Imp': { - 'ext': { - 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + ortb2Imp: { + ext: { + tid: '56e184c6-bde9-497b-b9b9-cf47a61381ee' } } -} - -const ORTB2_DEVICE = { - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, - {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' +}; + +const NATIVE_BID = { + bidId: '2d52001cabd529', + bidder: 'screencore', + adUnitCode: 'native-ad-unit', + transactionId: '77e184c6-bde9-497b-b9b9-cf47a61381ee', + params: { + placementId: 'testNativePlacement' }, - w: 980, - h: 1720, - dnt: 0, - ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/125.0.6422.80 Mobile/15E148 Safari/604.1', - language: 'en', - devicetype: 1, - make: 'Apple', - model: 'iPhone 12 Pro Max', - os: 'iOS', - osv: '17.4', - ext: {fiftyonedegrees_deviceId: '17595-133085-133468-18092'}, + mediaTypes: { + native: { + title: { required: true }, + image: { required: true }, + sponsoredBy: { required: false } + } + } }; const BIDDER_REQUEST = { - 'gdprConsent': { - 'consentString': 'consent_string', - 'gdprApplies': true - }, - 'gppString': 'gpp_string', - 'gppSid': [7], - 'uspConsent': 'consent_string', - 'refererInfo': { - 'page': 'https://www.greatsite.com', - 'ref': 'https://www.somereferrer.com' + refererInfo: { + page: 'https://www.example.com', + ref: 'https://www.referrer.com' }, - 'ortb2': { - 'site': { - 'content': { - 'language': 'en' - } - }, - 'regs': { - 'gpp': 'gpp_string', - 'gpp_sid': [7], - 'coppa': 0 - }, - 'device': ORTB2_DEVICE, + ortb2: { + device: { + w: 1920, + h: 1080, + language: 'en' + } } }; const SERVER_RESPONSE = { - body: { - cid: 'testcid123', - results: [{ - 'ad': '', - 'price': 0.8, - 'creativeId': '12610997325162499419', - 'exp': 30, - 'width': 300, - 'height': 250, - 'advertiserDomains': ['securepubads.g.doubleclick.net'], - 'cookies': [{ - 'src': 'https://sync.com', - 'type': 'iframe' - }, { - 'src': 'https://sync.com', - 'type': 'img' - }] - }] - } + body: [{ + requestId: '2d52001cabd527', + cpm: 0.8, + creativeId: '12610997325162499419', + ttl: 30, + currency: 'USD', + width: 300, + height: 250, + mediaType: 'banner', + ad: '', + adomain: ['securepubads.g.doubleclick.net'] + }] }; const VIDEO_SERVER_RESPONSE = { - body: { - 'cid': '635509f7ff6642d368cb9837', - 'results': [{ - 'ad': '', - 'advertiserDomains': ['screencore.io'], - 'exp': 60, - 'width': 545, - 'height': 307, - 'mediaType': 'video', - 'creativeId': '12610997325162499419', - 'price': 2, - 'cookies': [] - }] - } -}; - -const ORTB2_OBJ = { - "device": ORTB2_DEVICE, - "regs": {"coppa": 0, "gpp": "gpp_string", "gpp_sid": [7]}, - "site": {"content": {"language": "en"} - } + body: [{ + requestId: '2d52001cabd528', + cpm: 2, + creativeId: '12610997325162499419', + ttl: 60, + currency: 'USD', + width: 545, + height: 307, + mediaType: 'video', + vastXml: '', + adomain: ['screencore.io'] + }] }; const REQUEST = { data: { - width: 300, - height: 250, - bidId: '2d52001cabd527' + placements: [{ + bidId: '2d52001cabd527', + adFormat: 'banner', + sizes: [[300, 250], [300, 600]] + }] } }; -function getTopWindowQueryParams() { - try { - const parsedUrl = utils.parseUrl(window.top.document.URL, { decodeSearchAsString: true }); - return parsedUrl.search; - } catch (e) { - return ''; - } -} - describe('screencore bid adapter', function () { before(() => config.resetConfig()); after(() => config.resetConfig()); describe('validate spec', function () { - it('exists and is a function', function () { + it('should have isBidRequestValid as a function', function () { expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have buildRequests as a function', function () { expect(adapter.buildRequests).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have interpretResponse as a function', function () { expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); }); - it('exists and is a function', function () { + it('should have getUserSyncs as a function', function () { expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); }); - it('exists and is a string', function () { + it('should have code as a string', function () { expect(adapter.code).to.exist.and.to.be.a('string'); + expect(adapter.code).to.equal('screencore'); }); - it('exists and contains media types', function () { + it('should have supportedMediaTypes with BANNER, VIDEO, NATIVE', function () { expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(3); expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO, NATIVE]); }); + + it('should have gvlid', function () { + expect(adapter.gvlid).to.exist.and.to.equal(1473); + }); + + it('should have version', function () { + expect(adapter.version).to.exist.and.to.equal('1.0.0'); + }); }); describe('validate bid requests', function () { - it('should require cId', function () { + it('should return false when placementId and endpointId are missing', function () { const isValid = adapter.isBidRequestValid({ - params: { - pId: 'pid', - }, + bidId: '123', + params: {}, + mediaTypes: { banner: { sizes: [[300, 250]] } } }); expect(isValid).to.be.false; }); - it('should require pId', function () { + it('should return false when mediaTypes is missing', function () { const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid', - }, + bidId: '123', + params: { placementId: 'test' } }); expect(isValid).to.be.false; }); - it('should validate correctly', function () { + it('should return true when placementId is present with banner mediaType', function () { const isValid = adapter.isBidRequestValid({ - params: { - cId: 'cid', - pId: 'pid', - }, + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { banner: { sizes: [[300, 250]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when endpointId is present with banner mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { endpointId: 'test' }, + mediaTypes: { banner: { sizes: [[300, 250]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when placementId is present with video mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { video: { playerSize: [[640, 480]] } } + }); + expect(isValid).to.be.true; + }); + + it('should return true when placementId is present with native mediaType', function () { + const isValid = adapter.isBidRequestValid({ + bidId: '123', + params: { placementId: 'test' }, + mediaTypes: { native: { title: { required: true } } } }); expect(isValid).to.be.true; }); }); describe('build requests', function () { - let sandbox; - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - sandbox = sinon.createSandbox(); - sandbox.stub(Date, 'now').returns(1000); + it('should build banner request', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.exist; + expect(requests.method).to.equal('POST'); + expect(requests.url).to.include('screencore.io/prebid'); + expect(requests.data).to.exist; + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(BANNER); }); it('should build video request', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000, - }); const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain()}/prebid/multi/635509f7ff6642d368cb9837`, - data: { - adUnitCode: '63550ad1ff6642d368cba59dh5884270560', - bidFloor: 0.1, - bidId: '2d52001cabd527', - bidderVersion: adapter.version, - bidderRequestId: '12a8ae9ada9c13', - cb: 1000, - gdpr: 1, - gdprConsent: 'consent_string', - usPrivacy: 'consent_string', - gppString: 'gpp_string', - gppSid: [7], - prebidVersion: version, - transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - publisherId: '59ac17c192832d0011283fe3', - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - res: `${window.top.screen.width}x${window.top.screen.height}`, - schain: VIDEO_BID.schain, - sizes: ['545x307'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - { 'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0'] }, - { 'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119'] }, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - uqs: getTopWindowQueryParams(), - mediaTypes: { - video: { - api: [2], - context: 'instream', - linearity: 1, - maxduration: 60, - mimes: [ - 'video/mp4', - 'application/javascript' - ], - minduration: 0, - placement: 1, - playerSize: [[545, 307]], - protocols: [2, 3, 5, 6], - startdelay: 0 - } - }, - gpid: '', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - ortb2Imp: VIDEO_BID.ortb2Imp, - ortb2: ORTB2_OBJ, - placementId: "testBanner", - userData: [], - coppa: 0 - } - }); + expect(requests).to.exist; + expect(requests.method).to.equal('POST'); + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(VIDEO_BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(VIDEO); }); - it('should build banner request for each size', function () { - const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); - config.setConfig({ - bidderTimeout: 3000 - }); - const requests = adapter.buildRequests([BID], BIDDER_REQUEST); - expect(requests).to.have.length(1); - expect(requests[0]).to.deep.equal({ - method: 'POST', - url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, - data: { - gdprConsent: 'consent_string', - gdpr: 1, - gppString: 'gpp_string', - gppSid: [7], - usPrivacy: 'consent_string', - transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', - bidRequestsCount: 4, - bidderRequestsCount: 3, - bidderWinsCount: 1, - bidderTimeout: 3000, - bidderRequestId: '1fdb5ff1b6eaa7', - sizes: ['300x250', '300x600'], - sua: { - 'source': 2, - 'platform': { - 'brand': 'Android', - 'version': ['8', '0', '0'] - }, - 'browsers': [ - { 'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0'] }, - { 'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119'] }, - {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} - ], - 'mobile': 1, - 'model': 'SM-G955U', - 'bitness': '64', - 'architecture': '' - }, - device: ORTB2_DEVICE, - url: 'https%3A%2F%2Fwww.greatsite.com', - referrer: 'https://www.somereferrer.com', - cb: 1000, - bidFloor: 0.1, - bidId: '2d52001cabd527', - adUnitCode: 'div-gpt-ad-12345-0', - publisherId: '59ac17c192832d0011283fe3', - uniqueDealId: `${hashUrl}_${Date.now().toString()}`, - bidderVersion: adapter.version, - prebidVersion: version, - schain: BID.schain, - res: `${window.top.screen.width}x${window.top.screen.height}`, - mediaTypes: [BANNER], - gpid: '0123456789', - uqs: getTopWindowQueryParams(), - 'ext.param1': 'loremipsum', - 'ext.param2': 'dolorsitamet', - cat: [], - contentLang: 'en', - contentData: [], - isStorageAllowed: true, - pagecat: [], - ortb2Imp: BID.ortb2Imp, - ortb2: ORTB2_OBJ, - placementId: "testBanner", - userData: [], - coppa: 0 - } - }); + it('should build native request', function () { + const requests = adapter.buildRequests([NATIVE_BID], BIDDER_REQUEST); + expect(requests).to.exist; + expect(requests.data.placements).to.be.an('array'); + expect(requests.data.placements[0].bidId).to.equal(NATIVE_BID.bidId); + expect(requests.data.placements[0].adFormat).to.equal(NATIVE); }); - after(function () { - getGlobal().bidderSettings = {}; - sandbox.restore(); + it('should include gpid when available', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests.data.placements[0].gpid).to.equal('0123456789'); }); - }); - describe('getUserSyncs', function () { - it('should have valid user sync with iframeEnabled', function () { - const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); + it('should include placementId in placement when present', function () { + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests.data.placements[0].placementId).to.equal('testPlacement'); + expect(requests.data.placements[0].type).to.equal('publisher'); + }); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - }]); + it('should include endpointId in placement when placementId is not present', function () { + const bidWithEndpoint = { + bidId: '2d52001cabd530', + bidder: 'screencore', + adUnitCode: 'div-gpt-ad-endpoint', + transactionId: 'd881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + params: { + endpointId: 'testEndpointOnly' + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + }; + const requests = adapter.buildRequests([bidWithEndpoint], BIDDER_REQUEST); + expect(requests.data.placements[0].endpointId).to.equal('testEndpointOnly'); + expect(requests.data.placements[0].type).to.equal('network'); }); + }); - it('should have valid user sync with cid on response', function () { + describe('getUserSyncs', function () { + it('should return iframe sync when iframeEnabled', function () { + config.setConfig({ coppa: 0 }); const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - }]); + expect(result).to.be.an('array').with.length(1); + expect(result[0].type).to.equal('iframe'); + expect(result[0].url).to.include('https://cs.screencore.io/iframe?pbjs=1'); }); - it('should have valid user sync with pixelEnabled', function () { + it('should return image sync when pixelEnabled', function () { + config.setConfig({ coppa: 0 }); const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE]); - - expect(result).to.deep.equal([{ - 'url': 'https://cs.screencore.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=0', - 'type': 'image', - }]); + expect(result).to.be.an('array').with.length(1); + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.include('https://cs.screencore.io/image?pbjs=1'); }); - it('should have valid user sync with coppa 1 on response', function () { - config.setConfig({ - coppa: 1, - }); + it('should include coppa parameter', function () { + config.setConfig({ coppa: 1 }); const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); - expect(result).to.deep.equal([{ - type: 'iframe', - url: 'https://cs.screencore.io/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=&coppa=1', - }]); + expect(result[0].url).to.include('coppa=1'); }); - it('should generate url with consent data', function () { + it('should include gdpr consent when provided', function () { + config.setConfig({ coppa: 0 }); const gdprConsent = { gdprApplies: true, - consentString: 'consent_string', + consentString: 'consent_string' }; - const uspConsent = 'usp_string'; + const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE], gdprConsent); + expect(result[0].url).to.include('gdpr=1'); + expect(result[0].url).to.include('gdpr_consent=consent_string'); + }); + + it('should include gpp consent when provided', function () { + config.setConfig({ coppa: 0 }); const gppConsent = { gppString: 'gpp_string', - applicableSections: [7], + applicableSections: [7] }; - - const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); - - expect(result).to.deep.equal([{ - 'url': 'https://cs.screencore.io/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&coppa=1&gpp=gpp_string&gpp_sid=7', - 'type': 'image', - }]); + const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE], null, null, gppConsent); + expect(result[0].url).to.include('gpp=gpp_string'); + expect(result[0].url).to.include('gpp_sid=7'); }); }); describe('interpret response', function () { - it('should return empty array when there is no response', function () { - const responses = adapter.interpretResponse(null); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no ad', function () { - const responses = adapter.interpretResponse({ price: 1, ad: '' }); - expect(responses).to.be.empty; - }); - - it('should return empty array when there is no price', function () { - const responses = adapter.interpretResponse({ price: null, ad: 'great ad' }); + it('should return empty array when body is empty array', function () { + const responses = adapter.interpretResponse({ body: [] }); expect(responses).to.be.empty; }); it('should return an array of interpreted banner responses', function () { const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 0.8, - width: 300, - height: 250, - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 30, - ad: '', - meta: { - advertiserDomains: ['securepubads.g.doubleclick.net'], - }, - }); - }); - - it('should get meta from response metaData', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - serverResponse.body.results[0].metaData = { - advertiserDomains: ['screencore.io'], - agencyName: 'Agency Name', - }; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses[0].meta).to.deep.equal({ - advertiserDomains: ['screencore.io'], - agencyName: 'Agency Name', - }); + expect(responses[0].requestId).to.equal('2d52001cabd527'); + expect(responses[0].cpm).to.equal(0.8); + expect(responses[0].width).to.equal(300); + expect(responses[0].height).to.equal(250); + expect(responses[0].creativeId).to.equal('12610997325162499419'); + expect(responses[0].currency).to.equal('USD'); + expect(responses[0].ttl).to.equal(30); + expect(responses[0].ad).to.equal(''); + expect(responses[0].meta.advertiserDomains).to.deep.equal(['securepubads.g.doubleclick.net']); }); it('should return an array of interpreted video responses', function () { const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); - expect(responses[0]).to.deep.equal({ - requestId: '2d52001cabd527', - cpm: 2, - width: 545, - height: 307, - mediaType: 'video', - creativeId: '12610997325162499419', - currency: 'USD', - netRevenue: true, - ttl: 60, - vastXml: '', - meta: { - advertiserDomains: ['screencore.io'], - }, - }); - }); - - it('should take default TTL', function () { - const serverResponse = utils.deepClone(SERVER_RESPONSE); - delete serverResponse.body.results[0].exp; - const responses = adapter.interpretResponse(serverResponse, REQUEST); - expect(responses).to.have.length(1); - expect(responses[0].ttl).to.equal(300); + expect(responses[0].requestId).to.equal('2d52001cabd528'); + expect(responses[0].cpm).to.equal(2); + expect(responses[0].width).to.equal(545); + expect(responses[0].height).to.equal(307); + expect(responses[0].mediaType).to.equal('video'); + expect(responses[0].vastXml).to.equal(''); }); }); - describe('user id system', function () { - TEST_ID_SYSTEMS.forEach((idSystemProvider) => { - const id = Date.now().toString(); - const bid = utils.deepClone(BID); - - const userId = (function () { - switch (idSystemProvider) { - case 'lipb': - return { lipbid: id }; - case 'id5id': - return { uid: id }; - default: - return id; - } - })(); - - bid.userId = { - [idSystemProvider]: userId, - }; - - it(`should include 'uid.${idSystemProvider}' in request params`, function () { - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + describe('createDomain test', function () { + it('should return correct domain for US timezone', function () { + const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ + resolvedOptions: () => ({ timeZone: 'America/New_York' }) }); - }); - // testing bid.userIdAsEids handling - it("should include user ids from bid.userIdAsEids (length=1)", function() { - const bid = utils.deepClone(BID); - bid.userIdAsEids = [ - { - "source": "audigent.com", - "uids": [{"id": "fakeidi6j6dlc6e"}] - } - ] - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.audigent.com']).to.equal("fakeidi6j6dlc6e"); - }) - it("should include user ids from bid.userIdAsEids (length=2)", function() { - const bid = utils.deepClone(BID); - bid.userIdAsEids = [ - { - "source": "audigent.com", - "uids": [{"id": "fakeidi6j6dlc6e"}] - }, - { - "source": "rwdcntrl.net", - "uids": [{"id": "fakeid6f35197d5c", "atype": 1}] - } - ] - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.audigent.com']).to.equal("fakeidi6j6dlc6e"); - expect(requests[0].data['uid.rwdcntrl.net']).to.equal("fakeid6f35197d5c"); - }) - // testing user.ext.eid handling - it("should include user ids from user.ext.eid (length=1)", function() { - const bid = utils.deepClone(BID); - bid.user = { - ext: { - eids: [ - { - "source": "pubcid.org", - "uids": [{"id": "fakeid8888dlc6e"}] - } - ] - } - } - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.pubcid.org']).to.equal("fakeid8888dlc6e"); - }) - it("should include user ids from user.ext.eid (length=2)", function() { - const bid = utils.deepClone(BID); - bid.user = { - ext: { - eids: [ - { - "source": "pubcid.org", - "uids": [{"id": "fakeid8888dlc6e"}] - }, - { - "source": "adserver.org", - "uids": [{"id": "fakeid495ff1"}] - } - ] - } - } - const requests = adapter.buildRequests([bid], BIDDER_REQUEST); - expect(requests[0].data['uid.pubcid.org']).to.equal("fakeid8888dlc6e"); - expect(requests[0].data['uid.adserver.org']).to.equal("fakeid495ff1"); - }) - }); - - describe('alternate param names extractors', function () { - it('should return undefined when param not supported', function () { - const cid = extractCID({ 'c_id': '1' }); - const pid = extractPID({ 'p_id': '1' }); - const subDomain = extractSubDomain({ 'sub_domain': 'prebid' }); - expect(cid).to.be.undefined; - expect(pid).to.be.undefined; - expect(subDomain).to.be.undefined; - }); - it('should return value when param supported', function () { - const cid = extractCID({ 'cID': '1' }); - const pid = extractPID({ 'Pid': '2' }); - const subDomain = extractSubDomain({ 'subDOMAIN': 'prebid' }); - expect(cid).to.be.equal('1'); - expect(pid).to.be.equal('2'); - expect(subDomain).to.be.equal('prebid'); - }); - }); - - describe('unique deal id', function () { - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - }); - after(function () { - getGlobal().bidderSettings = {}; - }); - const key = 'myKey'; - let uniqueDealId; - beforeEach(() => { - uniqueDealId = getUniqueDealId(storage, key, 0); - }); + const domain = createDomain(); + expect(domain).to.equal('https://taqus.screencore.io'); - it('should get current unique deal id', function (done) { - // waiting some time so `now` will become past - setTimeout(() => { - const current = getUniqueDealId(storage, key); - expect(current).to.be.equal(uniqueDealId); - done(); - }, 200); - }); - - it('should get new unique deal id on expiration', function (done) { - setTimeout(() => { - const current = getUniqueDealId(storage, key, 100); - expect(current).to.not.be.equal(uniqueDealId); - done(); - }, 200); + stub.restore(); }); - }); - describe('storage utils', function () { - before(function () { - getGlobal().bidderSettings = { - screencore: { - storageAllowed: true, - }, - }; - }); - after(function () { - getGlobal().bidderSettings = {}; - }); - it('should get value from storage with create param', function () { - const now = Date.now(); - const clock = useFakeTimers({ - shouldAdvanceTime: true, - now, + it('should return correct domain for EU timezone', function () { + const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ + resolvedOptions: () => ({ timeZone: 'Europe/London' }) }); - setStorageItem(storage, 'myKey', 2020); - const { value, created } = getStorageItem(storage, 'myKey'); - expect(created).to.be.equal(now); - expect(value).to.be.equal(2020); - expect(typeof value).to.be.equal('number'); - expect(typeof created).to.be.equal('number'); - clock.restore(); - }); - - it('should get external stored value', function () { - const value = 'superman'; - window.localStorage.setItem('myExternalKey', value); - const item = getStorageItem(storage, 'myExternalKey'); - expect(item).to.be.equal(value); - }); - it('should parse JSON value', function () { - const data = JSON.stringify({ event: 'send' }); - const { event } = tryParseJSON(data); - expect(event).to.be.equal('send'); - }); + const domain = createDomain(); + expect(domain).to.equal('https://taqeu.screencore.io'); - it('should get original value on parse fail', function () { - const value = 21; - const parsed = tryParseJSON(value); - expect(typeof parsed).to.be.equal('number'); - expect(parsed).to.be.equal(value); + stub.restore(); }); - }); - describe('createDomain test', function () { - it('should return correct domain', function () { + it('should return correct domain for APAC timezone', function () { const stub = sinon.stub(Intl, 'DateTimeFormat').returns({ - resolvedOptions: () => ({ timeZone: 'America/New_York' }), + resolvedOptions: () => ({ timeZone: 'Asia/Tokyo' }) }); - const responses = createDomain(); - expect(responses).to.be.equal('https://taqus.screencore.io'); + const domain = createDomain(); + expect(domain).to.equal('https://taqapac.screencore.io'); stub.restore(); }); diff --git a/test/spec/modules/seenthisBrandStories_spec.js b/test/spec/modules/seenthisBrandStories_spec.js new file mode 100644 index 00000000000..c565a33fa88 --- /dev/null +++ b/test/spec/modules/seenthisBrandStories_spec.js @@ -0,0 +1,333 @@ +import { expect } from "chai"; +import { + addStyleToSingleChildAncestors, + applyAutoHeight, + applyFullWidth, + calculateMargins, + DEFAULT_MARGINS, + findAdWrapper, + getFrameByEvent, + SEENTHIS_EVENTS, +} from "modules/seenthisBrandStories.ts"; +import * as boundingClientRect from "../../../libraries/boundingClientRect/boundingClientRect.js"; +import * as utils from "../../../src/utils.js"; +import * as winDimensions from "src/utils/winDimensions.js"; + +describe("seenthisBrandStories", function () { + describe("constants", function () { + it("should have correct DEFAULT_MARGINS", function () { + expect(DEFAULT_MARGINS).to.equal("16px"); + }); + + it("should have correct SEENTHIS_EVENTS array", function () { + expect(SEENTHIS_EVENTS).to.be.an("array").with.length(9); + expect(SEENTHIS_EVENTS).to.include("@seenthis_storylines/ready"); + expect(SEENTHIS_EVENTS).to.include("@seenthis_enabled"); + expect(SEENTHIS_EVENTS).to.include("@seenthis_modal/opened"); + }); + }); + + describe("calculateMargins", function () { + let mockElement; + let getBoundingClientRectStub; + let getComputedStyleStub; + + beforeEach(function () { + mockElement = { + style: { + setProperty: sinon.stub(), + }, + }; + + getBoundingClientRectStub = sinon.stub( + boundingClientRect, + "getBoundingClientRect" + ); + getComputedStyleStub = sinon.stub(window, "getComputedStyle"); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should set margins correctly with non-zero values", function () { + getBoundingClientRectStub.returns({ left: 32, width: 300 }); + getComputedStyleStub.returns({ marginLeft: "16px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "-16px" + ) + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "32px") + ).to.be.true; + }); + + it("should use default margins when width is 0", function () { + getBoundingClientRectStub.returns({ left: 16, width: 0 }); + getComputedStyleStub.returns({ marginLeft: "0px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "16px") + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "16px" + ) + ).to.be.true; + }); + + it("should use default margins when margin left is 0", function () { + getBoundingClientRectStub.returns({ left: 16, width: 300 }); + getComputedStyleStub.returns({ marginLeft: "16px" }); + + calculateMargins(mockElement); + + expect( + mockElement.style.setProperty.calledWith("--storylines-margins", "16px") + ).to.be.true; + expect( + mockElement.style.setProperty.calledWith( + "--storylines-margin-left", + "16px" + ) + ).to.be.true; + }); + }); + + describe("getFrameByEvent", function () { + let getElementsByTagNameStub; + let mockIframes; + let mockEventSource; + + beforeEach(function () { + mockEventSource = { id: "frame2" }; + + mockIframes = [ + { contentWindow: { id: "frame1" } }, + { contentWindow: mockEventSource }, // This will match + { contentWindow: { id: "frame3" } }, + ]; + + getElementsByTagNameStub = sinon + .stub(document, "getElementsByTagName") + .returns(mockIframes); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should return iframe matching event source", function () { + const mockEvent = { + source: mockEventSource, // This should match mockIframes[1].contentWindow + }; + + const result = getFrameByEvent(mockEvent); + + expect(result).to.equal(mockIframes[1]); + expect(getElementsByTagNameStub.calledWith("iframe")).to.be.true; + }); + + it("should return undefined if no iframe matches", function () { + const mockEvent = { + source: { id: "nonexistent" }, // This won't match any iframe + }; + + const result = getFrameByEvent(mockEvent); + + expect(result).to.be.null; + }); + }); + + describe("addStyleToSingleChildAncestors", function () { + beforeEach(function () { + sinon + .stub(winDimensions, "getWinDimensions") + .returns({ innerWidth: 1024, innerHeight: 768 }); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should apply style to element when width is less than window width", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + width: "", // key exists + }, + offsetWidth: 400, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.calledWith("width", "100%")).to.be + .true; + }); + + it("should not apply style when element width equals window width", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 1024, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.called).to.be.false; + }); + + it("should recursively apply to single child ancestors", function () { + const grandParent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 800, + parentElement: null, + children: { length: 1 }, + }; + + const parent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 600, + parentElement: grandParent, + children: { length: 1 }, + }; + + const child = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 400, + parentElement: parent, + }; + + addStyleToSingleChildAncestors(child, { key: "width", value: "100%" }); + + expect(child.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(parent.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(grandParent.style.setProperty.calledWith("width", "100%")).to.be + .true; + }); + + it("should stop recursion when parent has multiple children", function () { + const parent = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 600, + parentElement: null, + children: { length: 2 }, // Multiple children + }; + + const child = { + style: { + setProperty: sinon.stub(), + width: "", + }, + offsetWidth: 400, + parentElement: parent, + }; + + addStyleToSingleChildAncestors(child, { key: "width", value: "100%" }); + + expect(child.style.setProperty.calledWith("width", "100%")).to.be.true; + expect(parent.style.setProperty.called).to.be.false; + }); + + it("should not apply style when key is not in element style", function () { + const mockElement = { + style: { + setProperty: sinon.stub(), + // 'width' key not present + }, + offsetWidth: 400, + parentElement: null, + }; + + addStyleToSingleChildAncestors(mockElement, { + key: "width", + value: "100%", + }); + + expect(mockElement.style.setProperty.called).to.be.false; + }); + }); + + describe("findAdWrapper", function () { + it("should return grandparent element", function () { + const grandParent = {}; + const parent = { parentElement: grandParent }; + const target = { parentElement: parent }; + + const result = findAdWrapper(target); + + expect(result).to.equal(grandParent); + }); + }); + + describe("applyFullWidth", function () { + let findAdWrapperStub; + let addStyleToSingleChildAncestorsStub; + + beforeEach(function () { + findAdWrapperStub = sinon.stub(); + addStyleToSingleChildAncestorsStub = sinon.stub(); + }); + + afterEach(function () { + sinon.restore(); + }); + + it("should call addStyleToSingleChildAncestors with width 100% when adWrapper exists", function () { + const mockTarget = {}; + + expect(() => applyFullWidth(mockTarget)).to.not.throw(); + }); + + it("should handle null adWrapper gracefully", function () { + const mockTarget = {}; + + expect(() => applyFullWidth(mockTarget)).to.not.throw(); + }); + }); + + describe("applyAutoHeight", function () { + it("should call addStyleToSingleChildAncestors with height auto when adWrapper exists", function () { + const mockTarget = {}; + + // Test that function executes without errors + expect(() => applyAutoHeight(mockTarget)).to.not.throw(); + }); + + it("should handle null adWrapper gracefully", function () { + const mockTarget = {}; + + expect(() => applyAutoHeight(mockTarget)).to.not.throw(); + }); + }); +}); diff --git a/test/spec/modules/semantiqRtdProvider_spec.js b/test/spec/modules/semantiqRtdProvider_spec.js index 8eef7a1fc34..d2c0e506edf 100644 --- a/test/spec/modules/semantiqRtdProvider_spec.js +++ b/test/spec/modules/semantiqRtdProvider_spec.js @@ -44,7 +44,7 @@ describe('semantiqRtdProvider', () => { expect(body.event_type).to.be.equal('pageImpression'); expect(body.page_impression_id).not.to.be.empty; expect(body.source).to.be.equal('semantiqPrebidModule'); - expect(body.url).to.be.equal('https://example.com/article'); + expect(body.page_url).to.be.equal('https://example.com/article'); }); it('uses the correct company ID', () => { diff --git a/test/spec/modules/sevioBidAdapter_spec.js b/test/spec/modules/sevioBidAdapter_spec.js index 60c66870e14..d82f0da1f6c 100644 --- a/test/spec/modules/sevioBidAdapter_spec.js +++ b/test/spec/modules/sevioBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/sevioBidAdapter.js'; - +import { config } from 'src/config.js'; const ENDPOINT_URL = 'https://req.adx.ws/prebid'; describe('sevioBidAdapter', function () { @@ -205,5 +205,307 @@ describe('sevioBidAdapter', function () { let result = spec.interpretResponse(serverResponseNative); expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponseNative)); }) + + it('should use bidRequest.params.keywords when provided', function () { + const singleBidRequest = [ + { + bidder: 'sevio', + params: { + zone: 'zoneId', + keywords: ['play', 'games'] + }, + mediaTypes: { + banner: { sizes: [[728, 90]] } + }, + bidId: 'bid-kw', + bidderRequestId: 'br-kw', + auctionId: 'auc-kw' + } + ]; + const bidderRequest = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'https://example.com', + stack: ['https://example.com'] + } + }; + + const requests = spec.buildRequests(singleBidRequest, bidderRequest); + expect(requests).to.be.an('array').that.is.not.empty; + expect(requests[0].data).to.have.property('keywords'); + expect(requests[0].data.keywords).to.have.property('tokens'); + expect(requests[0].data.keywords.tokens).to.deep.equal(['play', 'games']); + }); + }); + + it('should prefer ortb2.site.keywords when present on bidderRequest', function () { + const singleBidRequest = [ + { + bidder: 'sevio', + params: { + zone: 'zoneId' + }, + mediaTypes: { + banner: { sizes: [[300, 250]] } + }, + bidId: 'bid-kw-ortb', + bidderRequestId: 'br-kw-ortb', + auctionId: 'auc-kw-ortb' + } + ]; + const bidderRequestWithOrtb = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'https://example.com', + stack: ['https://example.com'] + }, + ortb2: { + site: { + keywords: ['keyword1', 'keyword2'] + } + } + }; + + const requests = spec.buildRequests(singleBidRequest, bidderRequestWithOrtb); + expect(requests).to.be.an('array').that.is.not.empty; + expect(requests[0].data).to.have.property('keywords'); + expect(requests[0].data.keywords).to.have.property('tokens'); + expect(requests[0].data.keywords.tokens).to.deep.equal(['keyword1', 'keyword2']); + }); + + // Minimal env shims some helpers rely on + Object.defineProperty(window, 'visualViewport', { + value: { width: 1200, height: 800 }, + configurable: true + }); + Object.defineProperty(window, 'screen', { + value: { width: 1920, height: 1080 }, + configurable: true + }); + + function mkBid(overrides) { + return Object.assign({ + bidId: 'bid-1', + bidder: 'sevio', + params: { zone: 'zone-123', referenceId: 'ref-abc', keywords: ['k1', 'k2'] }, + mediaTypes: { banner: { sizes: [[300, 250]] } }, + refererInfo: { page: 'https://example.com/page', referer: 'https://referrer.example' }, + userIdAsEids: [] + }, overrides || {}); + } + + const baseBidderRequest = { + timeout: 1200, + refererInfo: { page: 'https://example.com/page', referer: 'https://referrer.example' }, + gdprConsent: { consentString: 'TCF-STRING' }, + uspConsent: { uspString: '1NYN' }, + gppConsent: { consentString: 'GPP-STRING' }, + ortb2: { device: {}, ext: {} } + }; + + describe('Sevio adapter helper coverage via buildRequests (JS)', () => { + let stubs = []; + + afterEach(() => { + while (stubs.length) stubs.pop().restore(); + document.title = ''; + document.head.innerHTML = ''; + try { + Object.defineProperty(navigator, 'connection', { value: undefined, configurable: true }); + } catch (e) {} + }); + + it('getReferrerInfo → data.referer', () => { + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out).to.have.lengthOf(1); + expect(out[0].data.referer).to.equal('https://example.com/page'); + }); + + it('getPageTitle prefers top.title; falls back to og:title (top document)', () => { + window.top.document.title = 'Doc Title'; + let out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.pageTitle).to.equal('Doc Title'); + + window.top.document.title = ''; + const meta = window.top.document.createElement('meta'); + meta.setAttribute('property', 'og:title'); + meta.setAttribute('content', 'OG Title'); + window.top.document.head.appendChild(meta); + + out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.pageTitle).to.equal('OG Title'); + + meta.remove(); + }); + + it('getPageTitle cross-origin fallback (window.top throws) uses local document.*', function () { + document.title = 'Local Title'; + + // In jsdom, window.top === window; try to simulate cross-origin by throwing from getter. + let restored = false; + try { + const original = Object.getOwnPropertyDescriptor(window, 'top'); + Object.defineProperty(window, 'top', { + configurable: true, + get() { throw new Error('cross-origin'); } + }); + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.pageTitle).to.equal('Local Title'); + Object.defineProperty(window, 'top', original); + restored = true; + } catch (e) { + // Environment didn’t allow redefining window.top; skip this case + this.skip(); + } finally { + if (!restored) { + try { Object.defineProperty(window, 'top', { value: window, configurable: true }); } catch (e) {} + } + } + }); + + it('computeTTFB via navigation entries (top.performance) and cached within call', () => { + const perfTop = window.top.performance; + + const original = perfTop.getEntriesByType; + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, + value: (type) => (type === 'navigation' ? [{ responseStart: 152, requestStart: 100 }] : []) + }); + + const out = spec.buildRequests([mkBid({ bidId: 'A' }), mkBid({ bidId: 'B' })], baseBidderRequest); + expect(out).to.have.lengthOf(2); + expect(out[0].data.timeToFirstByte).to.equal('52'); + expect(out[1].data.timeToFirstByte).to.equal('52'); + + Object.defineProperty(perfTop, 'getEntriesByType', { configurable: true, writable: true, value: original }); + }); + + it('computeTTFB falls back to top.performance.timing when no navigation entries', () => { + const perfTop = window.top.performance; + const originalGetEntries = perfTop.getEntriesByType; + const originalTimingDesc = Object.getOwnPropertyDescriptor(perfTop, 'timing'); + + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, value: () => [] + }); + + Object.defineProperty(perfTop, 'timing', { + configurable: true, + value: { responseStart: 250, requestStart: 200 } + }); + + const out = spec.buildRequests([mkBid()], baseBidderRequest); + expect(out[0].data.timeToFirstByte).to.equal('50'); + + Object.defineProperty(perfTop, 'getEntriesByType', { + configurable: true, writable: true, value: originalGetEntries + }); + if (originalTimingDesc) { + Object.defineProperty(perfTop, 'timing', originalTimingDesc); + } else { + Object.defineProperty(perfTop, 'timing', { configurable: true, value: undefined }); + } + }); + + it('handles multiple sizes correctly', function () { + const multiSizeBidRequests = [ + { + bidder: 'sevio', + params: { zone: 'zoneId' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90], + [160, 600], + ] + } + }, + bidId: 'multi123', + } + ]; + + const bidderRequests = { + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'https://example.com', + stack: ['https://example.com'] + } + }; + + const request = spec.buildRequests(multiSizeBidRequests, bidderRequests); + const sizes = request[0].data.ads[0].sizes; + + expect(sizes).to.deep.equal([ + { width: 300, height: 250 }, + { width: 728, height: 90 }, + { width: 160, height: 600 }, + ]); + }); + }); + + describe('currency handling', function () { + let bidRequests; + let bidderRequests; + + beforeEach(function () { + bidRequests = [{ + bidder: 'sevio', + params: { zone: 'zoneId' }, + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bidId: '123' + }]; + + bidderRequests = { + refererInfo: { + referer: 'https://example.com', + page: 'https://example.com', + } + }; + }); + + afterEach(function () { + if (typeof config.resetConfig === 'function') { + config.resetConfig(); + } else if (typeof config.setConfig === 'function') { + config.setConfig({ currency: null }); + } + }); + + it('includes EUR currency when EUR is set in prebid config', function () { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); + + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload.currency).to.equal('EUR'); + }); + + it('includes GBP currency when GBP is set in prebid config', function () { + config.setConfig({ + currency: { + adServerCurrency: 'GBP' + } + }); + + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload.currency).to.equal('GBP'); + }); + + it('does NOT include currency when no currency config is set', function () { + const req = spec.buildRequests(bidRequests, bidderRequests); + const payload = req[0].data; + + expect(payload).to.not.have.property('currency'); + }); }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 42641ccca1f..dfe9c85e3a3 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -4,9 +4,9 @@ import * as sinon from 'sinon'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; import * as utils from 'src/utils'; -import { deepSetValue } from '../../../src/utils.js'; +import * as equativUtils from '../../../libraries/equativUtils/equativUtils.js'; import { getImpIdMap, setIsEqtvTest } from '../../../modules/sharethroughBidAdapter.js'; -import * as equativUtils from '../../../libraries/equativUtils/equativUtils.js' +import { deepSetValue } from '../../../src/utils.js'; const spec = newBidder(sharethroughAdapterSpec).getSpec(); @@ -73,7 +73,7 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', params: { pkey: 111, - equativNetworkId: 73 + equativNetworkId: 73, }, requestId: 'efgh5678', ortb2Imp: { @@ -81,7 +81,7 @@ describe('sharethrough adapter spec', function () { tid: 'zsfgzzg', }, }, - } + }, ]; const videoBidRequests = [ @@ -113,7 +113,7 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', params: { pkey: 111, - equativNetworkIdId: 73 + equativNetworkIdId: 73, }, requestId: 'abcd1234', ortb2Imp: { @@ -121,65 +121,71 @@ describe('sharethrough adapter spec', function () { tid: 'zsgzgzz', }, }, - } + }, ]; const nativeOrtbRequest = { - assets: [{ - id: 0, - required: 1, - title: { - len: 140 - } - }, - { - id: 1, - required: 1, - img: { - type: 3, - w: 300, - h: 600 - } - }, - { - id: 2, - required: 1, - data: { - type: 1 - } - }], + assets: [ + { + id: 0, + required: 1, + title: { + len: 140, + }, + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 600, + }, + }, + { + id: 2, + required: 1, + data: { + type: 1, + }, + }, + ], context: 1, - eventtrackers: [{ - event: 1, - methods: [1, 2] - }], + eventtrackers: [ + { + event: 1, + methods: [1, 2], + }, + ], plcmttype: 1, privacy: 1, ver: '1.2', }; - const nativeBidRequests = [{ - bidder: 'sharethrough', - adUnitCode: 'sharethrough_native_42', - bidId: 'bidId3', - sizes: [], - mediaTypes: { - native: { - ...nativeOrtbRequest + const nativeBidRequests = [ + { + bidder: 'sharethrough', + adUnitCode: 'sharethrough_native_42', + bidId: 'bidId3', + sizes: [], + mediaTypes: { + native: { + ...nativeOrtbRequest, + }, }, - }, - nativeOrtbRequest, - params: { - pkey: 777, - equativNetworkId: 73 - }, - requestId: 'sharethrough_native_reqid_42', - ortb2Imp: { - ext: { - tid: 'sharethrough_native_tid_42', + nativeOrtbRequest, + params: { + pkey: 777, + equativNetworkId: 73, + }, + requestId: 'sharethrough_native_reqid_42', + ortb2Imp: { + ext: { + tid: 'sharethrough_native_tid_42', + }, }, }, - }] + ]; beforeEach(() => { config.setConfig({ @@ -334,9 +340,9 @@ describe('sharethrough adapter spec', function () { hp: 1, }, ], - } - } - } + }, + }, + }, }, getFloor: () => ({ currency: 'USD', floor: 42 }), }, @@ -383,14 +389,14 @@ describe('sharethrough adapter spec', function () { mediaTypes: { banner: bannerBidRequests[0].mediaTypes.banner, video: videoBidRequests[0].mediaTypes.video, - native: nativeBidRequests[0].mediaTypes.native + native: nativeBidRequests[0].mediaTypes.native, }, sizes: [], nativeOrtbRequest, bidder: 'sharethrough', params: { pkey: 111, - equativNetworkId: 73 + equativNetworkId: 73, }, requestId: 'efgh5678', ortb2Imp: { @@ -403,8 +409,8 @@ describe('sharethrough adapter spec', function () { return { floor: 1.1 }; } return { floor: 0.9 }; - } - } + }, + }, ]; bidderRequest = { @@ -422,7 +428,7 @@ describe('sharethrough adapter spec', function () { afterEach(() => { setIsEqtvTest(null); - }) + }); describe('buildRequests', function () { describe('top level object', () => { @@ -631,32 +637,38 @@ describe('sharethrough adapter spec', function () { regs: { ext: { dsa: { - 'dsarequired': 1, - 'pubrender': 0, - 'datatopub': 1, - 'transparency': [{ - 'domain': 'good-domain', - 'dsaparams': [1, 2] - }, { - 'domain': 'bad-setup', - 'dsaparams': ['1', 3] - }] - } - } - } - } + dsarequired: 1, + pubrender: 0, + datatopub: 1, + transparency: [ + { + domain: 'good-domain', + dsaparams: [1, 2], + }, + { + domain: 'bad-setup', + dsaparams: ['1', 3], + }, + ], + }, + }, + }, + }; const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; expect(openRtbReq.regs.ext.dsa.dsarequired).to.equal(1); expect(openRtbReq.regs.ext.dsa.pubrender).to.equal(0); expect(openRtbReq.regs.ext.dsa.datatopub).to.equal(1); - expect(openRtbReq.regs.ext.dsa.transparency).to.deep.equal([{ - 'domain': 'good-domain', - 'dsaparams': [1, 2] - }, { - 'domain': 'bad-setup', - 'dsaparams': ['1', 3] - }]); + expect(openRtbReq.regs.ext.dsa.transparency).to.deep.equal([ + { + domain: 'good-domain', + dsaparams: [1, 2], + }, + { + domain: 'bad-setup', + dsaparams: ['1', 3], + }, + ]); }); }); @@ -723,7 +735,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -747,7 +759,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -761,7 +773,7 @@ describe('sharethrough adapter spec', function () { // act const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[0]; - const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr + const ACTUAL_BATTR_VALUES = builtRequest.data.imp[0].banner.battr; // assert expect(ACTUAL_BATTR_VALUES).to.deep.equal(EXPECTED_BATTR_VALUES); @@ -838,18 +850,34 @@ describe('sharethrough adapter spec', function () { it('should not set a property if no corresponding property is detected on mediaTypes.video', () => { // arrange const propertiesToConsider = [ - 'api', 'battr', 'companionad', 'companiontype', 'delivery', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'plcmt', 'protocols', 'skip', 'skipafter', 'skipmin', 'startdelay' - ] + 'api', + 'battr', + 'companionad', + 'companiontype', + 'delivery', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'plcmt', + 'protocols', + 'skip', + 'skipafter', + 'skipmin', + 'startdelay', + ]; // act - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { delete bidRequests[1].mediaTypes.video[propertyToConsider]; }); const builtRequest = spec.buildRequests(bidRequests, bidderRequest)[1]; const videoImp = builtRequest.data.imp[0].video; // assert - propertiesToConsider.forEach(propertyToConsider => { + propertiesToConsider.forEach((propertyToConsider) => { expect(videoImp[propertyToConsider]).to.be.undefined; }); }); @@ -990,30 +1018,27 @@ describe('sharethrough adapter spec', function () { describe('isEqtvTest', () => { it('should set publisher id if equativNetworkId param is present', () => { - const builtRequest = spec.buildRequests(multiImpBidRequests, bidderRequest)[0] - expect(builtRequest.data.site.publisher.id).to.equal(73) - }) + const builtRequest = spec.buildRequests(multiImpBidRequests, bidderRequest)[0]; + expect(builtRequest.data.site.publisher.id).to.equal(73); + }); it('should not set publisher id if equativNetworkId param is not present', () => { const bidRequest = { ...bidRequests[0], params: { ...bidRequests[0].params, - equativNetworkId: undefined - } - } + equativNetworkId: undefined, + }, + }; - const builtRequest = spec.buildRequests([bidRequest], bidderRequest)[0] - expect(builtRequest.data.site.publisher).to.equal(undefined) - }) + const builtRequest = spec.buildRequests([bidRequest], bidderRequest)[0]; + expect(builtRequest.data.site.publisher).to.equal(undefined); + }); it('should generate a 14-char id for each imp object', () => { - const request = spec.buildRequests( - bannerBidRequests, - bidderRequest - ); + const request = spec.buildRequests(bannerBidRequests, bidderRequest); - request[0].data.imp.forEach(imp => { + request[0].data.imp.forEach((imp) => { expect(imp.id).to.have.lengthOf(14); }); }); @@ -1022,24 +1047,21 @@ describe('sharethrough adapter spec', function () { const bids = [ { ...bannerBidRequests[0], - getFloor: ({ size }) => ({ floor: size[0] * size[1] / 100_000 }) - } + getFloor: ({ size }) => ({ floor: (size[0] * size[1]) / 100_000 }), + }, ]; - const request = spec.buildRequests( - bids, - bidderRequest - ); + const request = spec.buildRequests(bids, bidderRequest); expect(request[0].data.imp).to.have.lengthOf(2); const firstImp = request[0].data.imp[0]; - expect(firstImp.bidfloor).to.equal(300 * 250 / 100_000); + expect(firstImp.bidfloor).to.equal((300 * 250) / 100_000); expect(firstImp.banner.format).to.have.lengthOf(1); expect(firstImp.banner.format[0]).to.deep.equal({ w: 300, h: 250 }); const secondImp = request[0].data.imp[1]; - expect(secondImp.bidfloor).to.equal(300 * 600 / 100_000); + expect(secondImp.bidfloor).to.equal((300 * 600) / 100_000); expect(secondImp.banner.format).to.have.lengthOf(1); expect(secondImp.banner.format[0]).to.deep.equal({ w: 300, h: 600 }); }); @@ -1064,7 +1086,7 @@ describe('sharethrough adapter spec', function () { // expect(secondImp).to.not.have.property('native'); // expect(secondImp).to.have.property('video'); // }); - }) + }); it('should return correct native properties from ORTB converter', () => { if (FEATURES.NATIVE) { @@ -1074,19 +1096,19 @@ describe('sharethrough adapter spec', function () { const asset1 = assets[0]; expect(asset1.id).to.equal(0); expect(asset1.required).to.equal(1); - expect(asset1.title).to.deep.equal({ 'len': 140 }); + expect(asset1.title).to.deep.equal({ len: 140 }); const asset2 = assets[1]; expect(asset2.id).to.equal(1); expect(asset2.required).to.equal(1); - expect(asset2.img).to.deep.equal({ 'type': 3, 'w': 300, 'h': 600 }); + expect(asset2.img).to.deep.equal({ type: 3, w: 300, h: 600 }); const asset3 = assets[2]; expect(asset3.id).to.equal(2); expect(asset3.required).to.equal(1); - expect(asset3.data).to.deep.equal({ 'type': 1 }) + expect(asset3.data).to.deep.equal({ type: 1 }); } - }) + }); }); describe('interpretResponse', function () { @@ -1148,7 +1170,7 @@ describe('sharethrough adapter spec', function () { it('should set requestId from impIdMap when isEqtvTest is true', () => { setIsEqtvTest(true); - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1172,15 +1194,15 @@ describe('sharethrough adapter spec', function () { }; const impIdMap = getImpIdMap(); - impIdMap['aaaabbbbccccdd'] = 'abcd1234' + impIdMap['aaaabbbbccccdd'] = 'abcd1234'; const resp = spec.interpretResponse(response, request)[0]; - expect(resp.requestId).to.equal('abcd1234') - }) + expect(resp.requestId).to.equal('abcd1234'); + }); it('should set ttl when bid.exp is a number > 0', () => { - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1196,7 +1218,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: 100 + exp: 100, }, ], }, @@ -1206,10 +1228,10 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request)[0]; expect(resp.ttl).to.equal(100); - }) + }); it('should set ttl to 360 when bid.exp is a number <= 0', () => { - request = spec.buildRequests(bannerBidRequests, bidderRequest)[0] + request = spec.buildRequests(bannerBidRequests, bidderRequest)[0]; response = { body: { seatbid: [ @@ -1225,7 +1247,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, ], }, @@ -1235,16 +1257,16 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request)[0]; expect(resp.ttl).to.equal(360); - }) + }); it('should return correct properties when fledgeAuctionEnabled is true and isEqtvTest is false', () => { - request = spec.buildRequests(bidRequests, bidderRequest)[0] + request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { ext: { auctionConfigs: { - key: 'value' - } + key: 'value', + }, }, seatbid: [ { @@ -1259,7 +1281,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, { id: 'efgh5678', @@ -1271,7 +1293,7 @@ describe('sharethrough adapter spec', function () { dealid: 'deal', adomain: ['domain.com'], adm: 'markup', - exp: -1 + exp: -1, }, ], }, @@ -1281,8 +1303,8 @@ describe('sharethrough adapter spec', function () { const resp = spec.interpretResponse(response, request); expect(resp.bids.length).to.equal(2); - expect(resp.paapi).to.deep.equal({ 'key': 'value' }) - }) + expect(resp.paapi).to.deep.equal({ key: 'value' }); + }); }); describe('video', () => { @@ -1354,9 +1376,9 @@ describe('sharethrough adapter spec', function () { it('should set correct ortb property', () => { const resp = spec.interpretResponse(response, request)[0]; - expect(resp.native.ortb).to.deep.equal({ 'ad': 'ad' }) - }) - }) + expect(resp.native.ortb).to.deep.equal({ ad: 'ad' }); + }); + }); describe('meta object', () => { beforeEach(() => { @@ -1397,8 +1419,8 @@ describe('sharethrough adapter spec', function () { expect(bid.meta.brandName).to.be.null; expect(bid.meta.demandSource).to.be.null; expect(bid.meta.dchain).to.be.null; - expect(bid.meta.primaryCatId).to.be.null; - expect(bid.meta.secondaryCatIds).to.be.null; + expect(bid.meta.primaryCatId).to.equal(''); + expect(bid.meta.secondaryCatIds).to.be.an('array').that.is.empty; expect(bid.meta.mediaType).to.be.null; }); @@ -1482,15 +1504,14 @@ describe('sharethrough adapter spec', function () { it('should call handleCookieSync with correct parameters and return its result', () => { setIsEqtvTest(true); - const expectedResult = [ - { type: 'iframe', url: 'https://sync.example.com' }, - ]; + const expectedResult = [{ type: 'iframe', url: 'https://sync.example.com' }]; - handleCookieSyncStub.returns(expectedResult) + handleCookieSyncStub.returns(expectedResult); - const result = spec.getUserSyncs({ iframeEnabled: true }, - SAMPLE_RESPONSE, - { gdprApplies: true, vendorData: { vendor: { consents: {} } } }); + const result = spec.getUserSyncs({ iframeEnabled: true }, SAMPLE_RESPONSE, { + gdprApplies: true, + vendorData: { vendor: { consents: {} } }, + }); sinon.assert.calledWithMatch( handleCookieSyncStub, diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js index 443999989da..2dd33d7ef5b 100644 --- a/test/spec/modules/shinezRtbBidAdapter_spec.js +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -14,7 +14,7 @@ import { tryParseJSON, getUniqueDealId, } from '../../../libraries/vidazooUtils/bidderUtils.js'; -import {parseUrl, deepClone} from 'src/utils.js'; +import {parseUrl, deepClone, getWinDimensions} from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; @@ -428,7 +428,7 @@ describe('ShinezRtbBidAdapter', function () { bidderVersion: adapter.version, prebidVersion: version, schain: BID.schain, - res: `${window.top.screen.width}x${window.top.screen.height}`, + res: `${getWinDimensions().screen.width}x${getWinDimensions().screen.height}`, mediaTypes: [BANNER], gpid: '0123456789', uqs: getTopWindowQueryParams(), diff --git a/test/spec/modules/smartytechBidAdapter_spec.js b/test/spec/modules/smartytechBidAdapter_spec.js index fe6af7ff101..17e1bb59bed 100644 --- a/test/spec/modules/smartytechBidAdapter_spec.js +++ b/test/spec/modules/smartytechBidAdapter_spec.js @@ -1,6 +1,8 @@ import {expect} from 'chai'; -import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH} from 'modules/smartytechBidAdapter'; +import {spec, ENDPOINT_PROTOCOL, ENDPOINT_DOMAIN, ENDPOINT_PATH, getAliasUserId, storage} from 'modules/smartytechBidAdapter'; import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; +import sinon from 'sinon'; const BIDDER_CODE = 'smartytech'; @@ -182,6 +184,43 @@ function mockRefererData() { } } +function mockBidderRequestWithConsents() { + return { + refererInfo: { + page: 'https://some-test.page' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA', + addtlConsent: '1~1.35.41.101' + }, + uspConsent: '1YNN' + } +} + +function mockBidRequestWithUserIds(mediaType, size, customSizes) { + const requests = mockBidRequestListData(mediaType, size, customSizes); + return requests.map(request => ({ + ...request, + userIdAsEids: [ + { + source: 'unifiedid.com', + uids: [{ + id: 'test-unified-id', + atype: 1 + }] + }, + { + source: 'pubcid.org', + uids: [{ + id: 'test-pubcid', + atype: 1 + }] + } + ] + })); +} + function mockResponseData(requestData) { const data = {} requestData.data.forEach((request, index) => { @@ -229,20 +268,25 @@ describe('SmartyTechDSPAdapter: buildRequests', () => { }); it('correct request URL', () => { const request = spec.buildRequests(mockBidRequest, mockReferer); - expect(request.url).to.be.equal(`${ENDPOINT_PROTOCOL}://${ENDPOINT_DOMAIN}${ENDPOINT_PATH}`) + request.forEach(req => { + expect(req.url).to.be.equal(`${ENDPOINT_PROTOCOL}://${ENDPOINT_DOMAIN}${ENDPOINT_PATH}`) + }); }); it('correct request method', () => { const request = spec.buildRequests(mockBidRequest, mockReferer); - expect(request.method).to.be.equal(`POST`) + request.forEach(req => { + expect(req.method).to.be.equal(`POST`) + }); }); it('correct request data', () => { - const data = spec.buildRequests(mockBidRequest, mockReferer).data; - data.forEach((request, index) => { - expect(request.adUnitCode).to.be.equal(mockBidRequest[index].adUnitCode); - expect(request.banner).to.be.equal(mockBidRequest[index].mediaTypes.banner); - expect(request.bidId).to.be.equal(mockBidRequest[index].bidId); - expect(request.endpointId).to.be.equal(mockBidRequest[index].params.endpointId); - expect(request.referer).to.be.equal(mockReferer.refererInfo.page); + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + data.forEach((req, index) => { + expect(req.adUnitCode).to.be.equal(mockBidRequest[index].adUnitCode); + expect(req.banner).to.be.equal(mockBidRequest[index].mediaTypes.banner); + expect(req.bidId).to.be.equal(mockBidRequest[index].bidId); + expect(req.endpointId).to.be.equal(mockBidRequest[index].params.endpointId); + expect(req.referer).to.be.equal(mockReferer.refererInfo.page); }) }); }); @@ -256,9 +300,10 @@ describe('SmartyTechDSPAdapter: buildRequests banner custom size', () => { }); it('correct request data', () => { - const data = spec.buildRequests(mockBidRequest, mockReferer).data; - data.forEach((request, index) => { - expect(request.banner.sizes).to.be.equal(mockBidRequest[index].params.sizes); + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + data.forEach((req, index) => { + expect(req.banner.sizes).to.be.equal(mockBidRequest[index].params.sizes); }) }); }); @@ -272,9 +317,10 @@ describe('SmartyTechDSPAdapter: buildRequests video custom size', () => { }); it('correct request data', () => { - const data = spec.buildRequests(mockBidRequest, mockReferer).data; - data.forEach((request, index) => { - expect(request.video.sizes).to.be.equal(mockBidRequest[index].params.sizes); + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + data.forEach((req, index) => { + expect(req.video.sizes).to.be.equal(mockBidRequest[index].params.sizes); }) }); }); @@ -287,7 +333,7 @@ describe('SmartyTechDSPAdapter: interpretResponse', () => { beforeEach(() => { const brData = mockBidRequestListData('banner', 2, []); mockReferer = mockRefererData(); - request = spec.buildRequests(brData, mockReferer); + request = spec.buildRequests(brData, mockReferer)[0]; mockBidRequest = { data: brData } @@ -333,7 +379,7 @@ describe('SmartyTechDSPAdapter: interpretResponse video', () => { beforeEach(() => { const brData = mockBidRequestListData('video', 2, []); mockReferer = mockRefererData(); - request = spec.buildRequests(brData, mockReferer); + request = spec.buildRequests(brData, mockReferer)[0]; mockBidRequest = { data: brData } @@ -359,3 +405,210 @@ describe('SmartyTechDSPAdapter: interpretResponse video', () => { }); }); }); + +describe('SmartyTechDSPAdapter: buildRequests with user IDs', () => { + let mockBidRequest; + let mockReferer; + beforeEach(() => { + mockBidRequest = mockBidRequestWithUserIds('banner', 2, []); + mockReferer = mockRefererData(); + }); + + it('should include userIds when available', () => { + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req, index) => { + expect(req).to.have.property('userIds'); + expect(req.userIds).to.deep.equal(mockBidRequest[index].userIdAsEids); + }); + }); + + it('should not include userIds when not available', () => { + const bidRequestWithoutUserIds = mockBidRequestListData('banner', 2, []); + const request = spec.buildRequests(bidRequestWithoutUserIds, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.not.have.property('userIds'); + }); + }); + + it('should not include userIds when userIdAsEids is undefined', () => { + const bidRequestWithUndefinedUserIds = mockBidRequestListData('banner', 2, []).map(req => { + const {userIdAsEids, ...requestWithoutUserIds} = req; + return requestWithoutUserIds; + }); + const request = spec.buildRequests(bidRequestWithUndefinedUserIds, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.not.have.property('userIds'); + }); + }); + + it('should not include userIds when userIdAsEids is empty array', () => { + const bidRequestWithEmptyUserIds = mockBidRequestListData('banner', 2, []).map(req => ({ + ...req, + userIdAsEids: [] + })); + const request = spec.buildRequests(bidRequestWithEmptyUserIds, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.not.have.property('userIds'); + }); + }); +}); + +describe('SmartyTechDSPAdapter: buildRequests with consent data', () => { + let mockBidRequest; + let mockBidderRequest; + beforeEach(() => { + mockBidRequest = mockBidRequestListData('banner', 2, []); + mockBidderRequest = mockBidderRequestWithConsents(); + }); + + it('should include GDPR consent when available', () => { + const request = spec.buildRequests(mockBidRequest, mockBidderRequest); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.have.property('gdprConsent'); + expect(req.gdprConsent.gdprApplies).to.be.true; + expect(req.gdprConsent.consentString).to.equal('COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA'); + expect(req.gdprConsent.addtlConsent).to.equal('1~1.35.41.101'); + }); + }); + + it('should include USP consent when available', () => { + const request = spec.buildRequests(mockBidRequest, mockBidderRequest); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.have.property('uspConsent'); + expect(req.uspConsent).to.equal('1YNN'); + }); + }); + + it('should not include consent data when not available', () => { + const mockReferer = mockRefererData(); + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.not.have.property('gdprConsent'); + expect(req).to.not.have.property('uspConsent'); + }); + }); +}); + +describe('SmartyTechDSPAdapter: Alias User ID (auId)', () => { + let cookiesAreEnabledStub; + let getCookieStub; + let setCookieStub; + let generateUUIDStub; + + beforeEach(() => { + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + generateUUIDStub = sinon.stub(utils, 'generateUUID'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + getCookieStub.restore(); + setCookieStub.restore(); + generateUUIDStub.restore(); + }); + + it('should return null if cookies are not enabled', () => { + cookiesAreEnabledStub.returns(false); + const auId = getAliasUserId(); + expect(auId).to.be.null; + }); + + it('should return existing auId from cookie', () => { + const existingAuId = 'existing-uuid-1234'; + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(existingAuId); + + const auId = getAliasUserId(); + expect(auId).to.equal(existingAuId); + expect(generateUUIDStub.called).to.be.false; + }); + + it('should generate and store new auId if cookie does not exist', () => { + const newAuId = 'new-uuid-5678'; + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(null); + generateUUIDStub.returns(newAuId); + + const auId = getAliasUserId(); + expect(auId).to.equal(newAuId); + expect(generateUUIDStub.calledOnce).to.be.true; + expect(setCookieStub.calledOnce).to.be.true; + + // Check that setCookie was called with correct parameters + const setCookieCall = setCookieStub.getCall(0); + expect(setCookieCall.args[0]).to.equal('_smartytech_auid'); // cookie name + expect(setCookieCall.args[1]).to.equal(newAuId); // cookie value + expect(setCookieCall.args[3]).to.equal('Lax'); // sameSite + }); + + it('should generate and store new auId if cookie is empty string', () => { + const newAuId = 'new-uuid-9999'; + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(''); + generateUUIDStub.returns(newAuId); + + const auId = getAliasUserId(); + expect(auId).to.equal(newAuId); + expect(generateUUIDStub.calledOnce).to.be.true; + }); +}); + +describe('SmartyTechDSPAdapter: buildRequests with auId', () => { + let mockBidRequest; + let mockReferer; + let cookiesAreEnabledStub; + let getCookieStub; + + beforeEach(() => { + mockBidRequest = mockBidRequestListData('banner', 2, []); + mockReferer = mockRefererData(); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getCookieStub = sinon.stub(storage, 'getCookie'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + getCookieStub.restore(); + }); + + it('should include auId in bid request when available', () => { + const testAuId = 'test-auid-12345'; + cookiesAreEnabledStub.returns(true); + getCookieStub.returns(testAuId); + + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.have.property('auId'); + expect(req.auId).to.equal(testAuId); + }); + }); + + it('should not include auId when cookies are disabled', () => { + cookiesAreEnabledStub.returns(false); + + const request = spec.buildRequests(mockBidRequest, mockReferer); + const data = request.flatMap(resp => resp.data); + + data.forEach((req) => { + expect(req).to.not.have.property('auId'); + }); + }); +}); diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js index e65fbbd88ef..12b29d9003b 100644 --- a/test/spec/modules/sparteoBidAdapter_spec.js +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -1,11 +1,11 @@ -import {expect} from 'chai'; +import { expect } from 'chai'; import { deepClone, mergeDeep } from 'src/utils'; -import {spec as adapter} from 'modules/sparteoBidAdapter'; +import { spec as adapter } from 'modules/sparteoBidAdapter'; const CURRENCY = 'EUR'; const TTL = 60; const HTTP_METHOD = 'POST'; -const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const REQUEST_URL = 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com'; const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; const VALID_BID_BANNER = { @@ -79,6 +79,7 @@ const VALID_REQUEST_BANNER = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -123,6 +124,7 @@ const VALID_REQUEST_VIDEO = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -186,6 +188,7 @@ const VALID_REQUEST = { } }], 'site': { + 'domain': 'dev.sparteo.com', 'publisher': { 'ext': { 'params': { @@ -199,17 +202,26 @@ const VALID_REQUEST = { } }; +const ORTB2_GLOBAL = { + site: { + domain: 'dev.sparteo.com' + } +}; + const BIDDER_REQUEST = { - bids: [VALID_BID_BANNER, VALID_BID_VIDEO] -} + bids: [VALID_BID_BANNER, VALID_BID_VIDEO], + ortb2: ORTB2_GLOBAL +}; const BIDDER_REQUEST_BANNER = { - bids: [VALID_BID_BANNER] -} + bids: [VALID_BID_BANNER], + ortb2: ORTB2_GLOBAL +}; const BIDDER_REQUEST_VIDEO = { - bids: [VALID_BID_VIDEO] -} + bids: [VALID_BID_VIDEO], + ortb2: ORTB2_GLOBAL +}; describe('SparteoAdapter', function () { describe('isBidRequestValid', function () { @@ -250,54 +262,49 @@ describe('SparteoAdapter', function () { describe('buildRequests', function () { describe('Check method return', function () { + it('should return the right formatted banner requests', function () { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); if (FEATURES.VIDEO) { - it('should return the right formatted requests', function() { + it('should return the right formatted requests', function () { const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); delete request.data.id; expect(request).to.deep.equal(VALID_REQUEST); }); - } - - it('should return the right formatted banner requests', function() { - const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); - delete request.data.id; - expect(request).to.deep.equal(VALID_REQUEST_BANNER); - }); - - if (FEATURES.VIDEO) { - it('should return the right formatted video requests', function() { + it('should return the right formatted video requests', function () { const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); delete request.data.id; expect(request).to.deep.equal(VALID_REQUEST_VIDEO); }); - } - it('should return the right formatted request with endpoint test', function() { - const endpoint = 'https://bid-test.sparteo.com/auction'; + it('should return the right formatted request with endpoint test', function () { + const endpoint = 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com'; - const bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { - params: { - endpoint: endpoint - } - }); + const bids = deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]); + bids[0].params.endpoint = endpoint; - const requests = mergeDeep(deepClone(VALID_REQUEST)); + const expectedRequest = deepClone(VALID_REQUEST); + expectedRequest.url = endpoint; + expectedRequest.data.imp[0].ext.sparteo.params.endpoint = endpoint; - const request = adapter.buildRequests(bids, BIDDER_REQUEST); - requests.url = endpoint; - delete request.data.id; + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + delete request.data.id; - expect(requests).to.deep.equal(requests); - }); + expect(request).to.deep.equal(expectedRequest); + }); + } }); }); - describe('interpretResponse', function() { + describe('interpretResponse', function () { describe('Check method return', function () { - it('should return the right formatted response', function() { + it('should return the right formatted response', function () { const response = { body: { 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', @@ -458,9 +465,9 @@ describe('SparteoAdapter', function () { }); }); - describe('onBidWon', function() { + describe('onBidWon', function () { describe('Check methods succeed', function () { - it('should not throw error', function() { + it('should not throw error', function () { const bids = [ { requestId: '1a2b3c4d', @@ -500,16 +507,16 @@ describe('SparteoAdapter', function () { } ]; - bids.forEach(function(bid) { + bids.forEach(function (bid) { expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); }); }); }); }); - describe('getUserSyncs', function() { + describe('getUserSyncs', function () { describe('Check methods succeed', function () { - it('should return the sync url', function() { + it('should return the sync url', function () { const syncOptions = { 'iframeEnabled': true, 'pixelEnabled': false @@ -531,4 +538,426 @@ describe('SparteoAdapter', function () { }); }); }); + + describe('replaceMacros via buildRequests', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + + it('replaces macros for site traffic (site_domain only)', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'site.sparteo.com', + publisher: { domain: 'dev.sparteo.com' } + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=site.sparteo.com' + ); + }); + + it('uses site.page hostname when site.domain is missing', function () { + const ENDPOINT2 = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT2; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + page: 'https://www.dev.sparteo.com:3000/p/some?x=1' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com' + ); + }); + + it('omits domain query and leaves network_id empty when neither site nor app is present', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = '1234567a-eb1b-1fae-1d23-e1fbaef234cf'; + + const bidderReq = { bids: [bid], ortb2: {} }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=' + ); + }); + + it('sets site_domain=unknown when site.domain is null', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: null + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('replaces ${NETWORK_ID} with empty when undefined', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + delete bid.params.networkId; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'dev.sparteo.com' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&site_domain=dev.sparteo.com' + ); + }); + + it('replaces ${NETWORK_ID} with empty when null', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + bid.params.networkId = null; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { + domain: 'dev.sparteo.com' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&site_domain=dev.sparteo.com' + ); + }); + + it('appends &bundle=... and uses app_domain when app.bundle is present', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + app: { + domain: 'dev.sparteo.com', + bundle: 'com.sparteo.app' + } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('does not append &bundle when app is missing; uses site_domain when site exists', function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { domain: 'dev.sparteo.com' } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=dev.sparteo.com' + ); + }); + + it('prefers site over app when both are present', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { + site: { domain: 'site.sparteo.com' }, + app: { domain: 'app.sparteo.com', bundle: 'com.sparteo.app' } + } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=site.sparteo.com' + ); + expect(req.data.site?.publisher?.ext?.params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.app).to.be.undefined; + }); + + ['', ' ', 'null', 'NuLl'].forEach((val) => { + it(`app bundle "${val}" produces &bundle=unknown`, function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: val } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=unknown' + ); + }); + }); + + it('app domain missing becomes app_domain=unknown while keeping bundle', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: '', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=unknown&bundle=com.sparteo.app' + ); + }); + + it('uses network_id from app.publisher.ext for app-only traffic', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.data.site).to.be.undefined; + expect(req.data.app?.publisher?.ext?.params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('unparsable site.page yields site_domain=unknown', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { site: { page: 'not a url' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('literal "null" in site.page yields site_domain=unknown', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { site: { domain: '', page: 'null' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=unknown' + ); + }); + + it('does not create site on app-only request', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { + bids: [bid], + ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } + }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.data.site).to.be.undefined; + expect(req.data.app).to.exist; + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + + it('propagates adUnitCode into imp.ext.sparteo.params.adUnitCode', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const req = adapter.buildRequests([bid], { bids: [bid], ortb2: { site: { domain: 'dev.sparteo.com' } } }); + delete req.data.id; + + expect(req.data.imp[0]?.ext?.sparteo?.params?.adUnitCode).to.equal(bid.adUnitCode); + }); + + it('sets pbjsVersion and networkId under site root', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { bids: [bid], ortb2: { site: { domain: 'dev.sparteo.com' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + const params = req.data.site?.publisher?.ext?.params; + expect(params?.pbjsVersion).to.equal('$prebid.version$'); + expect(params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.app?.publisher?.ext?.params?.pbjsVersion).to.be.undefined; + }); + + it('sets pbjsVersion and networkId under app root', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + + const bidderReq = { bids: [bid], ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + const params = req.data.app?.publisher?.ext?.params; + expect(params?.pbjsVersion).to.equal('$prebid.version$'); + expect(params?.networkId).to.equal('1234567a-eb1b-1fae-1d23-e1fbaef234cf'); + expect(req.data.site).to.be.undefined; + }); + + it('app-only without networkId leaves network_id empty', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${APP_DOMAIN_QUERY}${BUNDLE_QUERY}'; + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + delete bid.params.networkId; + + const bidderReq = { bids: [bid], ortb2: { app: { domain: 'dev.sparteo.com', bundle: 'com.sparteo.app' } } }; + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + 'https://bid.sparteo.com/auction?network_id=&app_domain=dev.sparteo.com&bundle=com.sparteo.app' + ); + }); + }); + + describe('domain normalization (strip www., port, path, trim)', function () { + const ENDPOINT = 'https://bid.sparteo.com/auction?network_id=${NETWORK_ID}${SITE_DOMAIN_QUERY}'; + + const CASES = [ + { + label: 'strips leading "www." from site.domain', + site: { domain: 'www.dev.sparteo.com' }, + expected: 'dev.sparteo.com' + }, + { + label: 'trims whitespace and strips "www."', + site: { domain: ' www.dev.sparteo.com ' }, + expected: 'dev.sparteo.com' + }, + { + label: 'preserves non-"www" prefixes like "www2."', + site: { domain: 'www2.dev.sparteo.com' }, + expected: 'www2.dev.sparteo.com' + }, + { + label: 'removes port from site.page', + site: { page: 'https://dev.sparteo.com:8080/path?q=1' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes "www." and path from site.page', + site: { page: 'http://www.dev.sparteo.com/p?q=1' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes port when it appears in site.domain', + site: { domain: 'dev.sparteo.com:8443' }, + expected: 'dev.sparteo.com' + }, + { + label: 'removes accidental path in site.domain', + site: { domain: 'dev.sparteo.com/some/path' }, + expected: 'dev.sparteo.com' + } + ]; + + CASES.forEach(({ label, site, expected }) => { + it(label, function () { + const bid = deepClone(VALID_BID_BANNER); + bid.params.endpoint = ENDPOINT; + const bidderReq = { bids: [bid], ortb2: { site } }; + + const req = adapter.buildRequests([bid], bidderReq); + delete req.data.id; + + expect(req.url).to.equal( + `https://bid.sparteo.com/auction?network_id=1234567a-eb1b-1fae-1d23-e1fbaef234cf&site_domain=${expected}` + ); + }); + }); + }); }); diff --git a/test/spec/modules/ssp_genieeBidAdapter_spec.js b/test/spec/modules/ssp_genieeBidAdapter_spec.js index 980d97c4c12..ab7e99ab5e5 100644 --- a/test/spec/modules/ssp_genieeBidAdapter_spec.js +++ b/test/spec/modules/ssp_genieeBidAdapter_spec.js @@ -82,42 +82,56 @@ describe('ssp_genieeBidAdapter', function () { afterEach(function () { sandbox.restore(); + config.resetConfig(); }); describe('isBidRequestValid', function () { - it('should return true when params.zoneId exists and params.currency does not exist', function () { - expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + it('should return false when params.zoneId does not exist', function () { + expect(spec.isBidRequestValid({ ...BANNER_BID, params: {} })).to.be.false; }); - it('should return true when params.zoneId and params.currency exist and params.currency is JPY or USD', function () { - config.setConfig({ currency: { adServerCurrency: 'JPY' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.true; - config.setConfig({ currency: { adServerCurrency: 'USD' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.true; - }); + describe('when params.currency is specified', function() { + it('should return true if currency is USD', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'USD' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); - it('should return false when params.zoneId does not exist', function () { - expect(spec.isBidRequestValid({ ...BANNER_BID, params: {} })).to.be.false; + it('should return true if currency is JPY', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'JPY' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('should return false if currency is not supported (e.g., EUR)', function() { + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'EUR' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return true if currency is valid, ignoring adServerCurrency', function() { + config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + const bid = { ...BANNER_BID, params: { ...BANNER_BID.params, currency: 'USD' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); }); - it('should return false when params.zoneId and params.currency exist and params.currency is neither JPY nor USD', function () { - config.setConfig({ currency: { adServerCurrency: 'EUR' } }); - expect( - spec.isBidRequestValid({ - ...BANNER_BID, - params: { ...BANNER_BID.params }, - }) - ).to.be.false; + describe('when params.currency is NOT specified (fallback to adServerCurrency)', function() { + it('should return true if adServerCurrency is not set', function() { + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return true if adServerCurrency is JPY', function() { + config.setConfig({ currency: { adServerCurrency: 'JPY' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return true if adServerCurrency is USD', function() { + config.setConfig({ currency: { adServerCurrency: 'USD' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.true; + }); + + it('should return false if adServerCurrency is not supported (e.g., EUR)', function() { + config.setConfig({ currency: { adServerCurrency: 'EUR' } }); + expect(spec.isBidRequestValid(BANNER_BID)).to.be.false; + }); }); }); diff --git a/test/spec/modules/suimBidAdapter_spec.js b/test/spec/modules/suimBidAdapter_spec.js index e06e5875d7c..ca537ba131b 100644 --- a/test/spec/modules/suimBidAdapter_spec.js +++ b/test/spec/modules/suimBidAdapter_spec.js @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { spec } from 'modules/suimBidAdapter.js'; -const ENDPOINT = 'https://bid.suimad.com/api/v1/prebids'; -const SYNC_URL = 'https://bid.suimad.com/api/v1/logs/usersync'; +const ENDPOINT = 'https://ad.suimad.com/bid'; +const SYNC_URL = 'https://ad.suimad.com/usersync'; describe('SuimAdapter', function () { describe('isBidRequestValid', function () { @@ -81,7 +81,7 @@ describe('SuimAdapter', function () { describe('interpretResponse', function () { const bidResponse = { - bidId: '22a91eced2e93a', + requestId: '22a91eced2e93a', cpm: 300, currency: 'JPY', width: 300, @@ -115,7 +115,7 @@ describe('SuimAdapter', function () { const result = spec.interpretResponse({ body: bidResponse }, bidderRequests); expect(result).to.have.lengthOf(1); expect(result[0]).to.deep.equal({ - requestId: bidResponse.bid, + requestId: bidResponse.requestId, cpm: 300, currency: 'JPY', width: 300, diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 95d53c8b5fc..0433c9d5378 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -423,6 +423,26 @@ describe('tadvertisingBidAdapter', () => { expect(bid.mediaType).to.deep.equal("video"); }) + + it('should return empty array when response has no body', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const bidderResponse = { body: {} }; + + const interpretedBids = spec.interpretResponse(bidderResponse, bidRequest); + + expect(interpretedBids).to.deep.equal([]); + }) + + it('should return empty array when response has only id and ext.uss', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const bidderResponse = { body: { id: '10b1e33f-fddc-4621-a472-d7bff0529cbf', ext: { uss: 1 } } }; + + const interpretedBids = spec.interpretResponse(bidderResponse, bidRequest); + + expect(interpretedBids).to.deep.equal([]); + }) }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index d9d0004e1e0..3cd09d57a35 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -81,6 +81,7 @@ const c_SERVERRESPONSE_B = { cid: '01744fbb521e9fb10ffea926190effea', crid: 'a13cf884e66e7c660afec059c89d98b6', adomain: [ + 'adomain.com' ], }, ], @@ -112,6 +113,7 @@ const c_SERVERRESPONSE_V = { cid: '01744fbb521e9fb10ffea926190effea', crid: 'a13cf884e66e7c660afec059c89d98b6', adomain: [ + 'adomain.com' ], }, ], @@ -385,6 +387,16 @@ describe('Tappx bid adapter', function () { const bids = spec.interpretResponse(emptyServerResponse, c_BIDDERREQUEST_B); expect(bids).to.have.lengthOf(0); }); + + it('receive reponse with adomain', function () { + const bids_B = spec.interpretResponse(c_SERVERRESPONSE_B, c_BIDDERREQUEST_B); + const bid_B = bids_B[0]; + expect(bid_B.meta.advertiserDomains).to.deep.equal(['adomain.com']); + + const bids_V = spec.interpretResponse(c_SERVERRESPONSE_V, c_BIDDERREQUEST_V); + const bid_V = bids_V[0]; + expect(bid_V.meta.advertiserDomains).to.deep.equal(['adomain.com']); + }); }); /** diff --git a/test/spec/modules/toponBidAdapter_spec.js b/test/spec/modules/toponBidAdapter_spec.js new file mode 100644 index 00000000000..bf717e4b847 --- /dev/null +++ b/test/spec/modules/toponBidAdapter_spec.js @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import { spec } from "modules/toponBidAdapter.js"; +import * as utils from "src/utils.js"; + +describe("TopOn Adapter", function () { + const PREBID_VERSION = "$prebid.version$"; + const BIDDER_CODE = "topon"; + + let bannerBid = { + bidder: BIDDER_CODE, + params: { + pubid: "pub-uuid", + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + }; + + const validBidRequests = [bannerBid]; + const bidderRequest = { + bids: [bannerBid], + }; + + const bannerResponse = { + bid: [ + { + id: "6e976fc683e543d892160ee7d6f057d8", + impid: "1fabbf3c-b5e4-4b7d-9956-8112f92c1076", + price: 7.906274762781043, + nurl: "https://127.0.0.1:1381/prebid_tk?...", + burl: "https://127.0.0.1:1381/prebid_tk?...", + lurl: "https://127.0.0.1:1381/prebid_tk?...", + adm: `
✅ TopOn Mock Ad
300x250 🚫
`, + adid: "Ad538d326a-47f1-4c22-80f0-67684a713898", + cid: "110", + crid: "Creative32666aba-b5d3-4074-9ad1-d1702e9ba22b", + exp: 1800, + ext: {}, + mtype: 1, + }, + ], + }; + + const response = { + body: { + cur: "USD", + id: "aa2653ff-bd37-4fef-8085-2e444347af8c", + seatbid: [bannerResponse], + }, + }; + + it("should properly expose spec attributes", function () { + expect(spec.code).to.equal(BIDDER_CODE); + expect(spec.supportedMediaTypes).to.exist.and.to.be.an("array"); + expect(spec.isBidRequestValid).to.be.a("function"); + expect(spec.buildRequests).to.be.a("function"); + expect(spec.interpretResponse).to.be.a("function"); + }); + + describe("Bid validations", () => { + it("should return true if publisherId is present in params", () => { + const isValid = spec.isBidRequestValid(validBidRequests[0]); + expect(isValid).to.equal(true); + }); + + it("should return false if publisherId is missing", () => { + const bid = utils.deepClone(validBidRequests[0]); + delete bid.params.pubid; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); + + it("should return false if publisherId is not of type string", () => { + const bid = utils.deepClone(validBidRequests[0]); + bid.params.pubid = 10000; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.equal(false); + }); + }); + + describe("Requests", () => { + it("should correctly build an ORTB Bid Request", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + + expect(request).to.be.an("object"); + expect(request.method).to.equal("POST"); + expect(request.data).to.exist; + expect(request.data).to.be.an("object"); + expect(request.data.id).to.be.an("string"); + expect(request.data.id).to.not.be.empty; + }); + + it("should include prebid flag in request", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + + expect(request.data.ext).to.have.property("prebid"); + expect(request.data.ext.prebid).to.have.property("channel"); + expect(request.data.ext.prebid.channel).to.deep.equal({ + version: PREBID_VERSION, + source: "pbjs", + }); + expect(request.data.source.ext.prebid).to.equal(1); + }); + }); + + describe("Response", () => { + it("should parse banner adm and set bidResponse.ad, width, and height", () => { + const request = spec.buildRequests(validBidRequests, bidderRequest); + response.body.seatbid[0].bid[0].impid = request.data.imp[0].id; + const bidResponses = spec.interpretResponse(response, request); + + expect(bidResponses).to.be.an("array"); + expect(bidResponses[0]).to.exist; + expect(bidResponses[0].ad).to.exist; + expect(bidResponses[0].mediaType).to.equal("banner"); + expect(bidResponses[0].width).to.equal(300); + expect(bidResponses[0].height).to.equal(250); + }); + }); +}); diff --git a/test/spec/modules/uniquestWidgetBidAdapter_spec.js b/test/spec/modules/uniquest_widgetBidAdapter_spec.js similarity index 96% rename from test/spec/modules/uniquestWidgetBidAdapter_spec.js rename to test/spec/modules/uniquest_widgetBidAdapter_spec.js index b487fdd8de4..55a06a976bc 100644 --- a/test/spec/modules/uniquestWidgetBidAdapter_spec.js +++ b/test/spec/modules/uniquest_widgetBidAdapter_spec.js @@ -1,10 +1,10 @@ import { expect } from 'chai'; -import { spec } from 'modules/uniquestWidgetBidAdapter.js'; +import { spec } from 'modules/uniquest_widgetBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; const ENDPOINT = 'https://adpb.ust-ad.com/hb/prebid/widgets'; -describe('UniquestWidgetAdapter', function () { +describe('uniquest_widgetBidAdapter', function () { const adapter = newBidder(spec); describe('inherited functions', function () { diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 41e3b3f7373..a24f537625e 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -805,6 +805,17 @@ describe('VisxAdapter', function () { } }); }); + + it('if gpid is present payload must have gpid param', function () { + const firstBid = Object.assign({}, bidRequests[0]); + firstBid.ortb2Imp = { ext: { gpid: 'adunit-gpid-1' } } + const bids = [firstBid]; + const request = spec.buildRequests(bids, bidderRequest); + const postData = request.data; + + expect(postData).to.be.an('object'); + expect(postData.imp[0].ext.gpid).to.equal('adunit-gpid-1'); + }); }); describe('buildRequests (multiple media types w/ unsupported video+outstream)', function () { diff --git a/test/spec/modules/welectBidAdapter_spec.js b/test/spec/modules/welectBidAdapter_spec.js index a0e3c797ac9..b9abf3613a2 100644 --- a/test/spec/modules/welectBidAdapter_spec.js +++ b/test/spec/modules/welectBidAdapter_spec.js @@ -178,7 +178,8 @@ describe('WelectAdapter', function () { ttl: 120, vastUrl: 'some vast url', height: 640, - width: 320 + width: 320, + mediaType: 'video' } } } @@ -213,7 +214,8 @@ describe('WelectAdapter', function () { requestId: 'some bid id', ttl: 120, vastUrl: 'some vast url', - width: 320 + width: 320, + mediaType: 'video' } it('if response reflects unavailability, should be empty', function () { diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index bd0d2157ce0..8963671c412 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -8,9 +8,10 @@ import { BANNER, NATIVE } from '../../../src/mediaTypes.js'; import { addFPDToBidderRequest } from '../../helpers/fpd.js'; import * as webdriver from '../../../libraries/webdriver/webdriver.js'; -describe('Yandex adapter', function () { - let sandbox; +const adUnitCode = 'adUnit-123'; +let sandbox; +describe('Yandex adapter', function () { beforeEach(function () { sandbox = sinon.createSandbox(); @@ -65,12 +66,7 @@ describe('Yandex adapter', function () { let mockBidderRequest; beforeEach(function () { - mockBidRequests = [{ - bidId: 'bid123', - params: { - placementId: 'R-I-123456-2', - } - }]; + mockBidRequests = [getBidRequest()]; mockBidderRequest = { ortb2: { device: { @@ -85,6 +81,15 @@ describe('Yandex adapter', function () { } } }; + + sandbox.stub(frameElement, 'getBoundingClientRect').returns({ + left: 123, + top: 234, + }); + }); + + afterEach(function () { + removeElement(adUnitCode); }); it('should set site.content.language from document language if it is not set', function () { @@ -110,6 +115,37 @@ describe('Yandex adapter', function () { expect(requests[0].data.imp[0].displaymanagerver).to.not.be.undefined; }); + it('should return banner coordinates', function () { + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.imp[0].ext.coords.x).to.equal(123); + expect(requests[0].data.imp[0].ext.coords.y).to.equal(234); + }); + + it('should return page scroll coordinates', function () { + createElementVisible(adUnitCode); + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.device.ext.scroll.top).to.equal(0); + expect(requests[0].data.device.ext.scroll.left).to.equal(0); + }); + + it('should return correct visible', function () { + createElementVisible(adUnitCode); + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + expect(requests[0].data.imp[0].ext.isvisible).to.equal(true); + }); + + it('should return correct visible for hidden element', function () { + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + createElementHidden(adUnitCode); + expect(requests[0].data.imp[0].ext.isvisible).to.equal(false); + }); + + it('should return correct visible for invisible element', function () { + const requests = spec.buildRequests(mockBidRequests, mockBidderRequest); + createElementInvisible(adUnitCode); + expect(requests[0].data.imp[0].ext.isvisible).to.equal(false); + }); + /** @type {import('../../../src/auction').BidderRequest} */ const bidderRequest = { ortb2: { @@ -157,6 +193,34 @@ describe('Yandex adapter', function () { }, }; + it('create a valid banner request with custom domain', function () { + config.setConfig({ + yandex: { + domain: 'yandex.tr', + }, + }); + + const bannerRequest = getBidRequest(); + bannerRequest.getFloor = () => ({ + currency: 'EUR', + // floor: 0.5 + }); + + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request).to.exist; + const { method, url } = request; + + expect(method).to.equal('POST'); + + const parsedRequestUrl = utils.parseUrl(url); + + expect(parsedRequestUrl.hostname).to.equal('yandex.tr'); + }) + it('creates a valid banner request', function () { const bannerRequest = getBidRequest(); bannerRequest.getFloor = () => ({ @@ -177,7 +241,7 @@ describe('Yandex adapter', function () { const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl - expect(parsedRequestUrl.hostname).to.equal('yandex.ru'); + expect(parsedRequestUrl.hostname).to.equal('yandex.com'); expect(parsedRequestUrl.pathname).to.equal('/ads/prebid/123'); expect(query['imp-id']).to.equal('1'); @@ -957,7 +1021,87 @@ function getBidRequest(extra = {}) { return { ...getBidConfig(), bidId: 'bidid-1', - adUnitCode: 'adUnit-123', + adUnitCode, ...extra, }; } + +/** + * Creates a basic div element with specified ID and appends it to document body + * @param {string} id - The ID to assign to the div element + * @returns {HTMLDivElement} The created div element + */ +function createElement(id) { + const div = document.createElement('div'); + div.id = id; + div.style.width = '50px'; + div.style.height = '50px'; + div.style.background = 'black'; + + // Adjust frame dimensions if running within an iframe + if (frameElement) { + frameElement.style.width = '100px'; + frameElement.style.height = '100px'; + } + + window.document.body.appendChild(div); + + return div; +} + +/** + * Creates a visible element with mocked bounding client rect for testing + * @param {string} id - The ID to assign to the div element + * @returns {HTMLDivElement} The created div with mocked geometry + */ +function createElementVisible(id) { + const element = createElement(id); + // Mock client rect to simulate visible position in viewport + sandbox.stub(element, 'getBoundingClientRect').returns({ + x: 10, + y: 10, + }); + return element; +} + +/** + * Creates a completely hidden element (not rendered) using display: none + * @param {string} id - The ID to assign to the div element + * @returns {HTMLDivElement} The created hidden div element + */ +function createElementInvisible(id) { + const element = document.createElement('div'); + element.id = id; + element.style.display = 'none'; + + window.document.body.appendChild(element); + return element; +} + +/** + * Creates an invisible but space-reserved element using visibility: hidden + * with mocked bounding client rect for testing + * @param {string} id - The ID to assign to the div element + * @returns {HTMLDivElement} The created hidden div with mocked geometry + */ +function createElementHidden(id) { + const element = createElement(id); + element.style.visibility = 'hidden'; + // Mock client rect to simulate hidden element's geometry + sandbox.stub(element, 'getBoundingClientRect').returns({ + x: 100, + y: 100, + }); + return element; +} + +/** + * Removes an element from the DOM by its ID if it exists + * @param {string} id - The ID of the element to remove + */ +function removeElement(id) { + const element = document.getElementById(id); + if (element) { + element.remove(); + } +} diff --git a/test/spec/modules/yieldoneBidAdapter_spec.js b/test/spec/modules/yieldoneBidAdapter_spec.js index 7e3ae3b944a..ea9ca43edbf 100644 --- a/test/spec/modules/yieldoneBidAdapter_spec.js +++ b/test/spec/modules/yieldoneBidAdapter_spec.js @@ -733,5 +733,19 @@ describe('yieldoneBidAdapter', function () { gdprApplies: true, })).to.be.undefined; }); + + it('should skip sync request for bot-like user agents', function () { + const originalUA = navigator.userAgent; + try { + Object.defineProperty(navigator, 'userAgent', { + value: 'Googlebot/2.1 (+http://www.google.com/bot.html)', + configurable: true + }); + + expect(spec.getUserSyncs({'iframeEnabled': true})).to.be.undefined; + } finally { + Object.defineProperty(navigator, 'userAgent', { value: originalUA, configurable: true }); + } + }); }); }); diff --git a/test/spec/ortbConverter/pbsExtensions/params_spec.js b/test/spec/ortbConverter/pbsExtensions/params_spec.js index d1b36c18b49..bad5307b9af 100644 --- a/test/spec/ortbConverter/pbsExtensions/params_spec.js +++ b/test/spec/ortbConverter/pbsExtensions/params_spec.js @@ -1,20 +1,9 @@ import {setImpBidParams} from '../../../../libraries/pbsExtensions/processors/params.js'; describe('pbjs -> ortb bid params to imp[].ext.prebid.BIDDER', () => { - let bidderRegistry, index, adUnit; - beforeEach(() => { - bidderRegistry = {}; - adUnit = {code: 'mockAdUnit'}; - index = { - getAdUnit() { - return adUnit; - } - } - }); - - function setParams(bidRequest, context, deps = {}) { + function setParams(bidRequest = {}) { const imp = {}; - setImpBidParams(imp, bidRequest, context, Object.assign({bidderRegistry, index}, deps)) + setImpBidParams(imp, bidRequest) return imp; } diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index e37a41ad510..0be07704ed2 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -2099,50 +2099,113 @@ describe('adapterManager tests', function () { return adapterManager.makeBidRequests(adUnits, 0, 'mockAuctionId', 1000, [], ortb2Fragments); } - it('should NOT populate source.tid with auctionId', () => { - const reqs = makeRequests(); - expect(reqs[0].ortb2.source.tid).to.not.equal('mockAuctionId'); - }); - it('should override source.tid if specified in FPD', () => { - const reqs = makeRequests({ - global: { - source: { - tid: 'tid' - } - }, - rubicon: { - source: { - tid: 'tid' - } - } + Object.entries({ + disabled() {}, + consistent() { + config.setConfig({ + enableTIDs: true, + consistentTIDs: true, + }) + }, + inconsistent() { + config.setConfig({ + enableTIDs: true + }) + } + }).forEach(([t, setup]) => { + describe(`when TIDs are ${t}`, () => { + beforeEach(setup); + afterEach(() => { + config.resetConfig() + }); + it('should respect source.tid from FPD', () => { + const reqs = makeRequests({ + global: { + source: { + tid: 'tid' + } + }, + bidder: { + rubicon: { + source: { + tid: 'tid2' + } + } + } + }); + reqs.forEach(req => { + expect(req.ortb2.source.tid).to.eql(req.bidderCode === 'rubicon' ? 'tid2' : 'tid'); + expect(req.ortb2.source.ext.tidSource).to.eql('pub'); + }); + }) + it('should respect publisher-provided ortb2Imp.ext.tid values', () => { + adUnits[1].ortb2Imp = {ext: {tid: 'pub-tid'}}; + const tidRequests = makeRequests().flatMap(br => br.bids).filter(req => req.adUnitCode === adUnits[1].code); + expect(tidRequests.length).to.eql(2); + tidRequests.forEach(req => { + expect(req.ortb2Imp.ext.tid).to.eql('pub-tid'); + expect(req.ortb2Imp.ext.tidSource).to.eql('pub'); + }) + }); + }) + }) + describe('when tids are enabled', () => { + beforeEach(() => { + config.setConfig({enableTIDs: true}); + }) + afterEach(() => { + config.resetConfig(); }); - reqs.forEach(req => { - expect(req.ortb2.source.tid).to.exist; - expect(req.ortb2.source.tid).to.not.eql('tid'); + + it('should populate source.tid', () => { + makeRequests().forEach(req => { + expect(req.ortb2.source.tid).to.exist; + }); + }) + + it('should generate ortb2Imp.ext.tid', () => { + makeRequests().flatMap(br => br.bids).forEach(req => { + expect(req.ortb2Imp.ext.tid).to.exist; + }) }); - expect(reqs[0].ortb2.source.tid).to.not.eql(reqs[1].ortb2.source.tid); - }); - it('should generate ortb2Imp.ext.tid', () => { - const reqs = makeRequests(); - const tids = new Set(reqs.flatMap(br => br.bids).map(b => b.ortb2Imp?.ext?.tid)); - expect(tids.size).to.eql(3); - }); - it('should override ortb2Imp.ext.tid if specified in FPD', () => { - adUnits[0].ortb2Imp = adUnits[1].ortb2Imp = { - ext: { - tid: 'tid' - } - }; - const reqs = makeRequests(); - expect(reqs[0].bids[0].ortb2Imp.ext.tid).to.not.eql('tid'); - }); - it('should use matching ext.tid if transactionId match', () => { - adUnits[1].transactionId = adUnits[0].transactionId; - const reqs = makeRequests(); - reqs.forEach(br => { - expect(new Set(br.bids.map(b => b.ortb2Imp.ext.tid)).size).to.eql(1); + + describe('and inconsistent', () => { + it('should NOT populate source.tid with auctionId', () => { + const reqs = makeRequests(); + expect(reqs[0].ortb2.source.tid).to.not.equal('mockAuctionId'); + expect(reqs[0].ortb2.source.ext.tidSource).to.eql('pbjs') + }); + it('should provide different source.tid to different bidders', () => { + const reqs = makeRequests(); + expect(reqs[0].ortb2.source.tid).to.not.equal(reqs[1].ortb2.source.tid); + }); + it('should provide different ortb2Imp.ext.tid to different bidders', () => { + const reqs = makeRequests().flatMap(br => br.bids).filter(br => br.adUnitCode === adUnits[1].code); + expect(reqs[0].ortb2Imp.ext.tid).to.not.eql(reqs[1].ortb2Imp.ext.tid); + reqs.forEach(req => { + expect(req.ortb2Imp.ext.tidSource).to.eql('pbjs'); + }) + }); + }); + describe('and consistent', () => { + beforeEach(() => { + config.setConfig({consistentTIDs: true}); + }); + it('should populate source.tid with auctionId', () => { + const reqs = makeRequests(); + expect(reqs[0].ortb2.source.tid).to.eql('mockAuctionId'); + expect(reqs[0].ortb2.source.ext.tidSource).to.eql('pbjsStable'); + }); + it('should provide the same ext.tid to all bidders', () => { + const reqs = makeRequests().flatMap(br => br.bids).filter(req => req.adUnitCode === adUnits[1].code); + expect(reqs[0].ortb2Imp.ext.tid).to.eql(reqs[1].ortb2Imp.ext.tid); + reqs.forEach(req => { + expect(req.ortb2Imp.ext.tid).to.exist; + expect(req.ortb2Imp.ext.tidSource).to.eql('pbjsStable'); + }) + }) }) - }); + }) describe('when the same bidder is routed to both client and server', () => { function route(next) { next.bail({ @@ -2552,6 +2615,7 @@ describe('adapterManager tests', function () { describe('gdpr consent module', function () { it('inserts gdprConsent object to bidRequest only when module was enabled', function () { + gdprDataHandler.enable(); gdprDataHandler.setConsentData({ consentString: 'abc123def456', consentRequired: true diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 72347beae9b..3ce5db91ef2 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -16,6 +16,7 @@ import {createBid} from '../../../../src/bidfactory.js'; import {hook, setupBeforeHookFnOnce} from '../../../../src/hook.js'; import {getHighestCpm} from '../../../../src/utils/reducers.js'; import {getGlobal} from '../../../../src/prebidGlobal.js'; +import { getAdUnitBidLimitMap } from '../../../../src/targeting.js'; function mkBid(bid) { return Object.assign(createBid(), bid); @@ -546,6 +547,12 @@ describe('targeting tests', function () { }); it('selects the top n number of bids when enableSendAllBids is true and and bitLimit is set', function () { + let getAdUnitsStub = sandbox.stub(auctionManager, 'getAdUnits').callsFake(() => ([ + { + code: '/123456/header-bid-tag-0', + }, + ])); + config.setConfig({ sendBidsControl: { bidLimit: 1 @@ -555,6 +562,7 @@ describe('targeting tests', function () { const targeting = targetingInstance.getAllTargeting(['/123456/header-bid-tag-0']); const limitedBids = Object.keys(targeting['/123456/header-bid-tag-0']).filter(key => key.indexOf(TARGETING_KEYS.PRICE_BUCKET + '_') !== -1) + getAdUnitsStub.restore(); expect(limitedBids.length).to.equal(1); }); @@ -583,8 +591,49 @@ describe('targeting tests', function () { expect(limitedBids.length).to.equal(2); }); + + it('getHighestCpmBidsFromBidPool calculates bids limit properly when bidLimit is a map', function () { + const bidLimit = { + 'adunit1': 2 + }; + const bids = [ + { ...bid1, bidderCode: 'rubicon', adUnitCode: 'adunit1' }, + { ...bid2, bidderCode: 'appnexus', adUnitCode: 'adunit1' }, + { ...bid3, bidderCode: 'dgads', adUnitCode: 'adunit1' }, + ]; + + const limitedBids = getHighestCpmBidsFromBidPool(bids, getHighestCpm, bidLimit); + + expect(limitedBids.length).to.equal(2); + }); }); + it('getAdUnitBidLimitMap returns correct map of adUnitCode to bidLimit', function() { + enableSendAllBids = true; + let getAdUnitsStub = sandbox.stub(auctionManager, 'getAdUnits').callsFake(() => ([ + { + code: 'adunit1', + bidLimit: 2 + }, + { + code: 'adunit2', + bidLimit: 5 + }, + { + code: 'adunit3' + } + ])); + + const adUnitBidLimitMap = getAdUnitBidLimitMap(['adunit1', 'adunit2', 'adunit3'], 0); + + expect(adUnitBidLimitMap).to.deep.equal({ + 'adunit1': 2, + 'adunit2': 5, + 'adunit3': undefined + }); + getAdUnitsStub.restore(); + }) + describe('targetingControls.allowZeroCpmBids', function () { let bid4; let bidderSettingsStorage; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index fbdc1cbc24e..3a439f7eb04 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -16,7 +16,7 @@ import * as auctionModule from 'src/auction.js'; import {resetAuctionState} from 'src/auction.js'; import {registerBidder} from 'src/adapters/bidderFactory.js'; import * as pbjsModule from 'src/prebid.js'; -import pbjs, {startAuction} from 'src/prebid.js'; +import pbjs, {resetQueSetup, startAuction} from 'src/prebid.js'; import {hook} from '../../../src/hook.js'; import {reset as resetDebugging} from '../../../src/debugging.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; @@ -246,20 +246,34 @@ describe('Unit: Prebid Module', function () { beforeEach(() => { ran = false; queue = pbjs[prop] = []; + resetQueSetup(); }); after(() => { pbjs.processQueue(); }) - function pushToQueue() { - queue.push(() => { ran = true }); + function pushToQueue(fn = () => { ran = true }) { + return new Promise((resolve) => { + queue.push(() => { + fn(); + resolve(); + }); + }) } - it(`should patch .push`, () => { + it(`should patch .push`, async () => { pbjs.processQueue(); - pushToQueue(); + await pushToQueue(); expect(ran).to.be.true; }); + + it('should respect insertion order', async () => { + const log = []; + pushToQueue(() => log.push(1)); + pbjs.processQueue(); + await pushToQueue(() => log.push(2)); + expect(log).to.eql([1, 2]); + }); }) }); }) @@ -2835,6 +2849,18 @@ describe('Unit: Prebid Module', function () { // only appnexus supports native expect(biddersCalled.length).to.equal(1); }); + + it('module bids should not be filtered out', async () => { + delete adUnits[0].mediaTypes.banner; + adUnits[0].bids.push({ + module: 'pbsBidAdapter', + ortb2Imp: {} + }); + + pbjs.requestBids({adUnits}); + await auctionStarted; + expect(adapterManager.callBids.getCall(0).args[0][0].bids.length).to.eql(2); + }) }); describe('part 2', function () { @@ -3859,19 +3885,32 @@ describe('Unit: Prebid Module', function () { utils.logError.restore(); }); - it('should run commands which are pushed into it', function() { + function push(cmd) { + return new Promise((resolve) => { + pbjs.cmd.push(() => { + try { + cmd(); + } finally { + resolve(); + } + }) + }) + } + + it('should run commands which are pushed into it', async function () { const cmd = sinon.spy(); - pbjs.cmd.push(cmd); + await push(cmd); assert.isTrue(cmd.called); }); - it('should log an error when given non-functions', function() { + it('should log an error when given non-functions', async function () { pbjs.cmd.push(5); + await push(() => null); assert.isTrue(utils.logError.calledOnce); }); - it('should log an error if the command passed into it fails', function() { - pbjs.cmd.push(function() { + it('should log an error if the command passed into it fails', async function () { + await push(function () { throw new Error('Failed function.'); }); assert.isTrue(utils.logError.calledOnce); diff --git a/test/spec/utils/cachedApiWrapper_spec.js b/test/spec/utils/cachedApiWrapper_spec.js new file mode 100644 index 00000000000..b354ed304b7 --- /dev/null +++ b/test/spec/utils/cachedApiWrapper_spec.js @@ -0,0 +1,57 @@ +import {CachedApiWrapper} from '../../../src/utils/cachedApiWrapper.js'; + +describe('cachedApiWrapper', () => { + let target, child, grandchild, wrapper; + beforeEach(() => { + grandchild = {}; + child = { + grandchild + }; + target = { + child + }; + wrapper = new CachedApiWrapper(() => target, { + prop1: true, + child: { + prop2: true, + grandchild: { + prop3: true + } + } + }) + }); + + it('should delegate to target', () => { + target.prop1 = 'value'; + expect(wrapper.obj.prop1).to.eql('value'); + }); + it('should cache result', () => { + target.prop1 = 'value'; + expect(wrapper.obj.prop1).to.eql('value'); + target.prop1 = 'newValue'; + expect(wrapper.obj.prop1).to.eql('value'); + }); + + it('should clear cache on reset', () => { + target.prop1 = 'value'; + expect(wrapper.obj.prop1).to.eql('value'); + target.prop1 = 'newValue'; + wrapper.reset(); + expect(wrapper.obj.prop1).to.eql('newValue'); + }); + + it('should unwrap wrappers in obj', () => { + grandchild.prop3 = 'value'; + expect(wrapper.obj.child.grandchild.prop3).to.eql('value'); + grandchild.prop3 = 'value'; + expect(wrapper.obj.child.grandchild.prop3).to.eql('value'); + }); + + it('should reset childrens cache', () => { + child.prop2 = 'value'; + expect(wrapper.obj.child.prop2).to.eql('value'); + wrapper.reset(); + child.prop2 = 'newValue'; + expect(wrapper.obj.child.prop2).to.eql('newValue'); + }) +}) diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 6db7a3561eb..1efdc5621f6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -1445,18 +1445,18 @@ describe('getWinDimensions', () => { clock.restore(); }); - it('should invoke fetchWinDimensions once per 20ms', () => { - const resetWinDimensionsSpy = sinon.spy(winDimensions.internal, 'fetchWinDimensions'); - getWinDimensions(); + it('should clear cache once per 20ms', () => { + const resetWinDimensionsSpy = sinon.spy(winDimensions.internal, 'reset'); + expect(getWinDimensions().innerHeight).to.exist; clock.tick(1); - getWinDimensions(); + expect(getWinDimensions().innerHeight).to.exist; clock.tick(1); - getWinDimensions(); + expect(getWinDimensions().innerHeight).to.exist; clock.tick(1); - getWinDimensions(); + expect(getWinDimensions().innerHeight).to.exist; sinon.assert.calledOnce(resetWinDimensionsSpy); clock.tick(18); - getWinDimensions(); + expect(getWinDimensions().innerHeight).to.exist; sinon.assert.calledTwice(resetWinDimensionsSpy); }); }); diff --git a/test/test_deps.js b/test/test_deps.js index e35e813a574..7047e775d9c 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -39,6 +39,8 @@ sinon.useFakeXMLHttpRequest = fakeXhr.useFakeXMLHttpRequest.bind(fakeXhr); sinon.createFakeServer = fakeServer.create.bind(fakeServer); sinon.createFakeServerWithClock = fakeServerWithClock.create.bind(fakeServerWithClock); +localStorage.clear(); + require('test/helpers/global_hooks.js'); require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); diff --git a/wdio.conf.js b/wdio.conf.js index d23fecd0b15..53ccd216b69 100644 --- a/wdio.conf.js +++ b/wdio.conf.js @@ -1,4 +1,5 @@ const shared = require('./wdio.shared.conf.js'); +const process = require('process'); const browsers = Object.fromEntries( Object.entries(require('./browsers.json')) @@ -28,7 +29,7 @@ function getCapabilities() { osVersion: browser.os_version, networkLogs: true, consoleLogs: 'verbose', - buildName: `Prebidjs E2E (${browser.browser} ${browser.browser_version}) ${new Date().toLocaleString()}` + buildName: process.env.BROWSERSTACK_BUILD_NAME }, acceptInsecureCerts: true, }); diff --git a/wdio.local.conf.js b/wdio.local.conf.js index 772448472bf..74c7ac3a3ee 100644 --- a/wdio.local.conf.js +++ b/wdio.local.conf.js @@ -1,4 +1,5 @@ const shared = require('./wdio.shared.conf.js'); +const process = require('process'); exports.config = { ...shared.config, @@ -9,5 +10,21 @@ exports.config = { args: ['headless', 'disable-gpu'], }, }, - ], + { + browserName: 'firefox', + 'moz:firefoxOptions': { + args: ['-headless'] + } + }, + { + browserName: 'msedge', + 'ms:edgeOptions': { + args: ['--headless'] + } + }, + { + browserName: 'safari technology preview' + } + ].filter((cap) => cap.browserName === (process.env.BROWSER ?? 'chrome')), + maxInstancesPerCapability: 1 };